diff options
author | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-03-13 16:21:49 -0500 |
---|---|---|
committer | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-03-13 16:21:49 -0500 |
commit | fe4135a68cfed92ef336eec663e9c42c2c97dcbc (patch) | |
tree | 2fcf583265be3c575086f3ce3183a3268c997538 /src/org/unitConverter/unit | |
parent | 761ba0b6627df8bc9f6ab41c94f349c84d378609 (diff) | |
parent | 184b7cc697ffc2dcbd49cfb3d0fd7b14bdac8803 (diff) |
Merge branch 'feature-settings-tab' into develop
Diffstat (limited to 'src/org/unitConverter/unit')
-rw-r--r-- | src/org/unitConverter/unit/BaseUnit.java | 86 | ||||
-rw-r--r-- | src/org/unitConverter/unit/FunctionalUnitlike.java | 72 | ||||
-rw-r--r-- | src/org/unitConverter/unit/LinearUnit.java | 328 | ||||
-rw-r--r-- | src/org/unitConverter/unit/LinearUnitValue.java | 341 | ||||
-rw-r--r-- | src/org/unitConverter/unit/MultiUnit.java | 160 | ||||
-rw-r--r-- | src/org/unitConverter/unit/MultiUnitTest.java | 106 | ||||
-rw-r--r-- | src/org/unitConverter/unit/NameSymbol.java | 317 | ||||
-rw-r--r-- | src/org/unitConverter/unit/Nameable.java | 59 | ||||
-rw-r--r-- | src/org/unitConverter/unit/SI.java | 490 | ||||
-rw-r--r-- | src/org/unitConverter/unit/Unit.java | 342 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitDatabase.java | 1269 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitDatabaseTest.java | 184 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitTest.java | 90 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitValue.java | 172 | ||||
-rw-r--r-- | src/org/unitConverter/unit/Unitlike.java | 260 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitlikeValue.java | 174 |
16 files changed, 3210 insertions, 1240 deletions
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java index d9f7965..6757bd0 100644 --- a/src/org/unitConverter/unit/BaseUnit.java +++ b/src/org/unitConverter/unit/BaseUnit.java @@ -23,8 +23,9 @@ import java.util.Set; /** * A unit that other units are defined by. * <p> - * Note that BaseUnits <b>must</b> have names and symbols. This is because they are used for toString code. Therefore, - * the Optionals provided by {@link #getPrimaryName} and {@link #getSymbol} will always contain a value. + * Note that BaseUnits <b>must</b> have names and symbols. This is because they + * are used for toString code. Therefore, the Optionals provided by + * {@link #getPrimaryName} and {@link #getSymbol} will always contain a value. * * @author Adrien Hopkins * @since 2019-10-16 @@ -33,63 +34,56 @@ public final class BaseUnit extends Unit { /** * Gets a base unit from the dimension it measures, its name and its symbol. * - * @param dimension - * dimension measured by this unit - * @param name - * name of unit - * @param symbol - * symbol of unit + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit * @return base unit * @since 2019-10-16 */ - public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol) { return new BaseUnit(dimension, name, symbol, new HashSet<>()); } - + /** * Gets a base unit from the dimension it measures, its name and its symbol. * - * @param dimension - * dimension measured by this unit - * @param name - * name of unit - * @param symbol - * symbol of unit + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit * @return base unit * @since 2019-10-21 */ - public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol, - final Set<String> otherNames) { + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol, final Set<String> otherNames) { return new BaseUnit(dimension, name, symbol, otherNames); } - + /** * The dimension measured by this base unit. */ private final BaseDimension dimension; - + /** * Creates the {@code BaseUnit}. * - * @param dimension - * dimension of unit - * @param primaryName - * name of unit - * @param symbol - * symbol of unit - * @throws NullPointerException - * if any argument is null + * @param dimension dimension of unit + * @param primaryName name of unit + * @param symbol symbol of unit + * @throws NullPointerException if any argument is null * @since 2019-10-16 */ - private BaseUnit(final BaseDimension dimension, final String primaryName, final String symbol, - final Set<String> otherNames) { + private BaseUnit(final BaseDimension dimension, final String primaryName, + final String symbol, final Set<String> otherNames) { super(primaryName, symbol, otherNames); - this.dimension = Objects.requireNonNull(dimension, "dimension must not be null."); + this.dimension = Objects.requireNonNull(dimension, + "dimension must not be null."); } - + /** - * Returns a {@code LinearUnit} with this unit as a base and a conversion factor of 1. This operation must be done - * in order to allow units to be created with operations. + * Returns a {@code LinearUnit} with this unit as a base and a conversion + * factor of 1. This operation must be done in order to allow units to be + * created with operations. * * @return this unit as a {@code LinearUnit} * @since 2019-10-16 @@ -97,17 +91,17 @@ public final class BaseUnit extends Unit { public LinearUnit asLinearUnit() { return LinearUnit.valueOf(this.getBase(), 1); } - + @Override - public double convertFromBase(final double value) { + protected double convertFromBase(final double value) { return value; } - + @Override - public double convertToBase(final double value) { + protected double convertToBase(final double value) { return value; } - + /** * @return dimension * @since 2019-10-16 @@ -115,21 +109,25 @@ public final class BaseUnit extends Unit { public final BaseDimension getBaseDimension() { return this.dimension; } - + @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : ""); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : ""); } - + @Override public BaseUnit withName(final NameSymbol ns) { Objects.requireNonNull(ns, "ns must not be null."); if (!ns.getPrimaryName().isPresent()) - throw new IllegalArgumentException("BaseUnits must have primary names."); + throw new IllegalArgumentException( + "BaseUnits must have primary names."); if (!ns.getSymbol().isPresent()) throw new IllegalArgumentException("BaseUnits must have symbols."); - return BaseUnit.valueOf(this.getBaseDimension(), ns.getPrimaryName().get(), ns.getSymbol().get(), + return BaseUnit.valueOf(this.getBaseDimension(), + ns.getPrimaryName().get(), ns.getSymbol().get(), ns.getOtherNames()); } } diff --git a/src/org/unitConverter/unit/FunctionalUnitlike.java b/src/org/unitConverter/unit/FunctionalUnitlike.java new file mode 100644 index 0000000..21c1fca --- /dev/null +++ b/src/org/unitConverter/unit/FunctionalUnitlike.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unitlike form that converts using two conversion functions. + * + * @since 2020-09-07 + */ +final class FunctionalUnitlike<V> extends Unitlike<V> { + /** + * A function that accepts a value in the unitlike form's base and returns a + * value in the unitlike form. + * + * @since 2020-09-07 + */ + private final DoubleFunction<V> converterFrom; + + /** + * A function that accepts a value in the unitlike form and returns a value + * in the unitlike form's base. + */ + private final ToDoubleFunction<V> converterTo; + + /** + * Creates the {@code FunctionalUnitlike}. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value in the unitlike form's + * base and returns a value in the unitlike form. + * @param converterTo function that accepts a value in the unitlike form + * and returns a value in the unitlike form's base. + * @throws NullPointerException if any argument is null + * @since 2019-05-22 + */ + protected FunctionalUnitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns, + DoubleFunction<V> converterFrom, ToDoubleFunction<V> converterTo) { + super(unitBase, ns); + this.converterFrom = converterFrom; + this.converterTo = converterTo; + } + + @Override + protected V convertFromBase(double value) { + return this.converterFrom.apply(value); + } + + @Override + protected double convertToBase(V value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 1e5ae53..b7f33d5 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -20,89 +20,101 @@ import java.util.Objects; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** - * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds. + * A unit that can be expressed as a product of its base and a number. For + * example, kilometres, inches and pounds. * * @author Adrien Hopkins * @since 2019-10-16 */ public final class LinearUnit extends Unit { /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' * - * @param unit - * unit to convert - * @param value - * value to convert + * @param unit unit to convert + * @param value value to convert * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 - * @throws NullPointerException - * if unit is null + * @throws NullPointerException if unit is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), NameSymbol.EMPTY); } - + /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' * - * @param unit - * unit to convert - * @param value - * value to convert - * @param ns - * name(s) and symbol of unit + * @param unit unit to convert + * @param value value to convert + * @param ns name(s) and symbol of unit * @return value expressed as a {@code LinearUnit} * @since 2019-10-21 - * @throws NullPointerException - * if unit or ns is null + * @throws NullPointerException if unit or ns is null */ - public static LinearUnit fromUnitValue(final Unit unit, final double value, final NameSymbol ns) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), ns); } - + /** - * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of - * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * @return the base unit associated with {@code unit}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unit unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * @return the base unit associated with {@code unitlike}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unitlike<?> unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by * @return product of base and conversion factor * @since 2019-10-16 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor) { + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor) { return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); } - + /** - * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of - * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by - * @param ns - * name(s) and symbol of unit + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @param ns name(s) and symbol of unit * @return product of base and conversion factor * @since 2019-10-21 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor, - final NameSymbol ns) { + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor, final NameSymbol ns) { return new LinearUnit(unitBase, conversionFactor, ns); } - + /** * The value of this unit as represented in its base form. Mathematically, * @@ -113,21 +125,20 @@ public final class LinearUnit extends Unit { * @since 2019-10-16 */ private final double conversionFactor; - + /** * Creates the {@code LinearUnit}. * - * @param unitBase - * base of linear unit - * @param conversionFactor - * conversion factor between base and unit + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit * @since 2019-10-16 */ - private LinearUnit(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor, final NameSymbol ns) { + private LinearUnit(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor, final NameSymbol ns) { super(unitBase, ns); this.conversionFactor = conversionFactor; } - + /** * {@inheritDoc} * @@ -137,7 +148,32 @@ public final class LinearUnit extends Unit { protected double convertFromBase(final double value) { return value / this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} value expressed in this unit to an + * {@code UncertainValue} value expressed in {@code other}. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + if (this.canConvertTo(other)) + return value.timesExact( + this.getConversionFactor() / other.getConversionFactor()); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + + } + /** * {@inheritDoc} * @@ -147,12 +183,20 @@ public final class LinearUnit extends Unit { protected double convertToBase(final double value) { return value * this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + /** * Divides this unit by a scalar. * - * @param divisor - * scalar to divide by + * @param divisor scalar to divide by * @return quotient * @since 2018-12-23 * @since v0.1.0 @@ -160,26 +204,26 @@ public final class LinearUnit extends Unit { public LinearUnit dividedBy(final double divisor) { return valueOf(this.getBase(), this.getConversionFactor() / divisor); } - + /** * Returns the quotient of this unit and another. * - * @param divisor - * unit to divide by + * @param divisor unit to divide by * @return quotient of two units - * @throws NullPointerException - * if {@code divisor} is null + * @throws NullPointerException if {@code divisor} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit dividedBy(final LinearUnit divisor) { Objects.requireNonNull(divisor, "other must not be null"); - + // divide the units - final ObjectProduct<BaseUnit> base = this.getBase().dividedBy(divisor.getBase()); - return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); + final ObjectProduct<BaseUnit> base = this.getBase() + .dividedBy(divisor.getBase()); + return valueOf(base, + this.getConversionFactor() / divisor.getConversionFactor()); } - + /** * {@inheritDoc} * @@ -191,9 +235,10 @@ public final class LinearUnit extends Unit { return false; final LinearUnit other = (LinearUnit) obj; return Objects.equals(this.getBase(), other.getBase()) - && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); } - + /** * @return conversion factor * @since 2019-10-16 @@ -201,7 +246,7 @@ public final class LinearUnit extends Unit { public double getConversionFactor() { return this.conversionFactor; } - + /** * {@inheritDoc} * @@ -209,18 +254,20 @@ public final class LinearUnit extends Unit { */ @Override public int hashCode() { - return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); + return 31 * this.getBase().hashCode() + + DecimalComparison.hash(this.getConversionFactor()); } - + /** - * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there is a {@code BaseUnit b} where + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where * {@code b.asLinearUnit().equals(this)} returns {@code true}.) * @since 2019-10-16 */ public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); } - + /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 @@ -228,70 +275,73 @@ public final class LinearUnit extends Unit { public boolean isCoherent() { return this.getConversionFactor() == 1; } - + /** * Returns the difference of this unit and another. * <p> - * Two units can be subtracted if they have the same base. Note that {@link #canConvertTo} can be used to determine - * this. If {@code subtrahend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be subtracted if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code subtrahend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. * </p> * - * @param subtrahend - * unit to subtract + * @param subtrahend unit to subtract * @return difference of units - * @throws IllegalArgumentException - * if {@code subtrahend} is not compatible for subtraction as described above - * @throws NullPointerException - * if {@code subtrahend} is null + * @throws IllegalArgumentException if {@code subtrahend} is not compatible + * for subtraction as described above + * @throws NullPointerException if {@code subtrahend} is null * @since 2019-03-17 * @since v0.2.0 */ - public LinearUnit minus(final LinearUnit subtrahendend) { - Objects.requireNonNull(subtrahendend, "addend must not be null."); - + public LinearUnit minus(final LinearUnit subtrahend) { + Objects.requireNonNull(subtrahend, "addend must not be null."); + // reject subtrahends that cannot be added to this unit - if (!this.getBase().equals(subtrahendend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); - + if (!this.getBase().equals(subtrahend.getBase())) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", this, + subtrahend)); + // subtract the units - return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() - subtrahend.getConversionFactor()); } - + /** * Returns the sum of this unit and another. * <p> - * Two units can be added if they have the same base. Note that {@link #canConvertTo} can be used to determine this. - * If {@code addend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be added if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code addend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. * </p> * - * @param addend - * unit to add + * @param addend unit to add * @return sum of units - * @throws IllegalArgumentException - * if {@code addend} is not compatible for addition as described above - * @throws NullPointerException - * if {@code addend} is null + * @throws IllegalArgumentException if {@code addend} is not compatible for + * addition as described above + * @throws NullPointerException if {@code addend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit plus(final LinearUnit addend) { Objects.requireNonNull(addend, "addend must not be null."); - + // reject addends that cannot be added to this unit if (!this.getBase().equals(addend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this, + addend)); + // add the units - return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() + addend.getConversionFactor()); } - + /** * Multiplies this unit by a scalar. * - * @param multiplier - * scalar to multiply by + * @param multiplier scalar to multiply by * @return product * @since 2018-12-23 * @since v0.1.0 @@ -299,39 +349,39 @@ public final class LinearUnit extends Unit { public LinearUnit times(final double multiplier) { return valueOf(this.getBase(), this.getConversionFactor() * multiplier); } - + /** * Returns the product of this unit and another. * - * @param multiplier - * unit to multiply by + * @param multiplier unit to multiply by * @return product of two units - * @throws NullPointerException - * if {@code multiplier} is null + * @throws NullPointerException if {@code multiplier} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit times(final LinearUnit multiplier) { Objects.requireNonNull(multiplier, "other must not be null"); - + // multiply the units - final ObjectProduct<BaseUnit> base = this.getBase().times(multiplier.getBase()); - return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); + final ObjectProduct<BaseUnit> base = this.getBase() + .times(multiplier.getBase()); + return valueOf(base, + this.getConversionFactor() * multiplier.getConversionFactor()); } - + /** * Returns this unit but to an exponent. * - * @param exponent - * exponent to exponentiate unit to + * @param exponent exponent to exponentiate unit to * @return exponentiated unit * @since 2019-01-15 * @since v0.1.0 */ public LinearUnit toExponent(final int exponent) { - return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); + return valueOf(this.getBase().toExponent(exponent), + Math.pow(this.conversionFactor, exponent)); } - + /** * @return a string providing a definition of this unit * @since 2019-10-21 @@ -339,49 +389,53 @@ public final class LinearUnit extends Unit { @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "") + ", " - + Double.toString(this.conversionFactor) + " * " + this.getBase().toString(u -> u.getSymbol().get()); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", " + Double.toString(this.conversionFactor) + " * " + + this.getBase().toString(u -> u.getSymbol().get()); } - + @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); } - + /** * Returns the result of applying {@code prefix} to this unit. * <p> - * If this unit and the provided prefix have a primary name, the returned unit will have a primary name (prefix's - * name + unit's name). <br> - * If this unit and the provided prefix have a symbol, the returned unit will have a symbol. <br> - * This method ignores alternate names of both this unit and the provided prefix. + * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name). <br> + * If this unit and the provided prefix have a symbol, the returned unit will + * have a symbol. <br> + * This method ignores alternate names of both this unit and the provided + * prefix. * - * @param prefix - * prefix to apply + * @param prefix prefix to apply * @return unit with prefix * @since 2019-03-18 * @since v0.2.0 - * @throws NullPointerException - * if prefix is null + * @throws NullPointerException if prefix is null */ public LinearUnit withPrefix(final UnitPrefix prefix) { final LinearUnit unit = this.times(prefix.getMultiplier()); - + // create new name and symbol, if possible final String name; - if (this.getPrimaryName().isPresent() && prefix.getPrimaryName().isPresent()) { + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); } else { name = null; } - + final String symbol; if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { symbol = prefix.getSymbol().get() + this.getSymbol().get(); } else { symbol = null; } - + return unit.withName(NameSymbol.ofNullable(name, symbol)); } } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java new file mode 100644 index 0000000..8de734e --- /dev/null +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -0,0 +1,341 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; + +/** + * A possibly uncertain value expressed in a linear unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class LinearUnitValue { + public static final LinearUnitValue ONE = getExact(SI.ONE, 1); + + /** + * Gets an exact {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, + final double value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); + } + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, + final UncertainDouble value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); + } + + private final LinearUnit unit; + + private final UncertainDouble value; + + /** + * @param unit unit to express as + * @param value value to express + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { + this.unit = unit; + this.value = value; + } + + /** + * @return this value as a {@code UnitValue}. All uncertainty information is + * removed from the returned value. + * @since 2020-08-04 + */ + public final UnitValue asUnitValue() { + return UnitValue.of(this.unit, this.value.value()); + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); + } + + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); + } + + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + * + * @since 2020-07-26 + * @see #equals(Object, boolean) + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + * <p> + * If avoidFPErrors is true, this method will attempt to avoid floating-point + * errors, at the cost of not always being transitive. + * + * @since 2020-07-28 + */ + public boolean equals(final Object obj, final boolean avoidFPErrors) { + if (!avoidFPErrors) + return this.equals(obj); + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && DecimalComparison.equals(this.unit.convertToBase(this.value), + other.unit.convertToBase(other.value)); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null + || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final UncertainDouble getValue() { + return this.value; + } + + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), + this.unit.convertToBase(this.getValue())); + } + + /** + * Returns the difference of this value and another, expressed in this + * value's unit + * + * @param subtrahend value to subtract + * @return difference of values + * @throws IllegalArgumentException if {@code subtrahend} has a unit that is + * not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", + this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.minus(otherConverted.value)); + } + + /** + * Returns the sum of this value and another, expressed in this value's unit + * + * @param addend value to add + * @return sum of values + * @throws IllegalArgumentException if {@code addend} has a unit that is not + * compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this.unit, + addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.value.isExact()); + } + + /** + * Returns a string representing the object. <br> + * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + * <p> + * If showUncertainty is true, strings like "35 ± 8" are shown instead of + * single numbers. + * <p> + * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional<String> primaryName = this.unit.getPrimaryName(); + final Optional<String> symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final UncertainDouble baseValue = this.unit.convertToBase(this.value); + + // get rounded strings + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + + // create string + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); + } +} diff --git a/src/org/unitConverter/unit/MultiUnit.java b/src/org/unitConverter/unit/MultiUnit.java new file mode 100644 index 0000000..a1623f8 --- /dev/null +++ b/src/org/unitConverter/unit/MultiUnit.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.unitConverter.math.ObjectProduct; + +/** + * A combination of units, like "5 foot + 7 inch". All but the last units should + * have a whole number value associated with them. + * + * @since 2020-10-02 + */ +public final class MultiUnit extends Unitlike<List<Double>> { + /** + * Creates a {@code MultiUnit} from its units. It will not have a name or + * symbol. + * + * @since 2020-10-03 + */ + public static final MultiUnit of(LinearUnit... units) { + return of(Arrays.asList(units)); + } + + /** + * Creates a {@code MultiUnit} from its units. It will not have a name or + * symbol. + * + * @since 2020-10-03 + */ + public static final MultiUnit of(List<LinearUnit> units) { + if (units.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + final ObjectProduct<BaseUnit> unitBase = units.get(0).getBase(); + for (final LinearUnit unit : units) { + if (!unitBase.equals(unit.getBase())) + throw new IllegalArgumentException( + "All units must have the same base."); + } + return new MultiUnit(new ArrayList<>(units), unitBase, NameSymbol.EMPTY); + } + + /** + * The units that make up this value. + */ + private final List<LinearUnit> units; + + /** + * Creates a {@code MultiUnit}. + * + * @since 2020-10-03 + */ + private MultiUnit(List<LinearUnit> units, ObjectProduct<BaseUnit> unitBase, + NameSymbol ns) { + super(unitBase, ns); + this.units = units; + } + + @Override + protected List<Double> convertFromBase(double value) { + final List<Double> values = new ArrayList<>(this.units.size()); + double temp = value; + + for (final LinearUnit unit : this.units.subList(0, + this.units.size() - 1)) { + values.add(Math.floor(temp / unit.getConversionFactor())); + temp %= unit.getConversionFactor(); + } + + values.add(this.units.size() - 1, + this.units.get(this.units.size() - 1).convertFromBase(temp)); + + return values; + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2020-10-03 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final <U extends Unitlike<V>, V> V convertTo(U other, + double... values) { + final List<Double> valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2020-10-03 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(Unit other, double... values) { + final List<Double> valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + @Override + protected double convertToBase(List<Double> value) { + if (value.size() != this.units.size()) + throw new IllegalArgumentException("Wrong number of values for " + + this.units.size() + "-unit MultiUnit."); + + double baseValue = 0; + for (int i = 0; i < this.units.size(); i++) { + baseValue += value.get(i) * this.units.get(i).getConversionFactor(); + } + return baseValue; + } +} diff --git a/src/org/unitConverter/unit/MultiUnitTest.java b/src/org/unitConverter/unit/MultiUnitTest.java new file mode 100644 index 0000000..5ea9d07 --- /dev/null +++ b/src/org/unitConverter/unit/MultiUnitTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Test; + +/** + * Tests related to the {@code MultiUnit}. + * + * @since 2020-10-03 + */ +class MultiUnitTest { + + @Test + final void testConvert() { + final Random rng = ThreadLocalRandom.current(); + final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + assertEquals(1702.0, footInch.convertTo(SI.METRE.withPrefix(SI.MILLI), + Arrays.asList(5.0, 7.0)), 1.0); + + for (int i = 0; i < 1000; i++) { + final double feet = rng.nextInt(1000); + final double inches = rng.nextDouble() * 12; + final double millimetres = feet * 304.8 + inches * 25.4; + + final List<Double> feetAndInches = SI.METRE.withPrefix(SI.MILLI) + .convertTo(footInch, millimetres); + assertEquals(feet, feetAndInches.get(0), 1e-10); + assertEquals(inches, feetAndInches.get(1), 1e-10); + } + } + + /** + * Test method for + * {@link org.unitConverter.unit.MultiUnit#convertFromBase(double)}. + */ + @Test + final void testConvertFromBase() { + final Random rng = ThreadLocalRandom.current(); + final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + // 1.7 m =~ 5' + 7" + final List<Double> values = footInch.convertFromBase(1.7018); + + assertEquals(5, values.get(0)); + assertEquals(7, values.get(1), 1e-12); + + for (int i = 0; i < 1000; i++) { + final double feet = rng.nextInt(1000); + final double inches = rng.nextDouble() * 12; + final double metres = feet * 0.3048 + inches * 0.0254; + + final List<Double> feetAndInches = footInch.convertFromBase(metres); + assertEquals(feet, feetAndInches.get(0), 1e-10); + assertEquals(inches, feetAndInches.get(1), 1e-10); + } + } + + /** + * Test method for + * {@link org.unitConverter.unit.MultiUnit#convertToBase(java.util.List)}. + */ + @Test + final void testConvertToBase() { + final Random rng = ThreadLocalRandom.current(); + final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT, + BritishImperial.Length.INCH); + + // 1.7 m =~ 5' + 7" + assertEquals(1.7018, footInch.convertToBase(Arrays.asList(5.0, 7.0)), + 1e-12); + + for (int i = 0; i < 1000; i++) { + final double feet = rng.nextInt(1000); + final double inches = rng.nextDouble() * 12; + final double metres = feet * 0.3048 + inches * 0.0254; + + assertEquals(metres, + footInch.convertToBase(Arrays.asList(feet, inches)), 1e-12); + } + } +} diff --git a/src/org/unitConverter/unit/NameSymbol.java b/src/org/unitConverter/unit/NameSymbol.java index 96fab45..8d8302a 100644 --- a/src/org/unitConverter/unit/NameSymbol.java +++ b/src/org/unitConverter/unit/NameSymbol.java @@ -19,6 +19,7 @@ package org.unitConverter.unit; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -30,227 +31,208 @@ import java.util.Set; * @since 2019-10-21 */ public final class NameSymbol { - public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), Optional.empty(), new HashSet<>()); - + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and no other names. + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName * - * @param name - * name to use - * @param symbol - * symbol to use - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException - * if name or symbol is null + * Ensure that otherNames is a copy of the inputted argument. */ - public static final NameSymbol of(final String name, final String symbol) { - return new NameSymbol(Optional.of(name), Optional.of(symbol), new HashSet<>()); + private static final NameSymbol create(final String name, + final String symbol, final Set<String> otherNames) { + final Optional<String> primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + final Iterator<String> it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); } - + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use + * @param name name to use + * @param symbol symbol to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if name or symbol is null */ - public static final NameSymbol of(final String name, final String symbol, final Set<String> otherNames) { + public static final NameSymbol of(final String name, final String symbol) { return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>(Objects.requireNonNull(otherNames, "otherNames must not be null."))); + new HashSet<>()); } - + /** - * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ - public static final NameSymbol of(final String name, final String symbol, final String... otherNames) { + public static final NameSymbol of(final String name, final String symbol, + final Set<String> otherNames) { return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, "otherNames must not be null.")))); - } - - /** - * Gets a {@code NameSymbol} with a primary name, a symbol and an additional name. - * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use - * @param name2 - * alternate name - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, final String name2) { - final Set<String> otherNames = new HashSet<>(); - otherNames.add(Objects.requireNonNull(name2, "name2 must not be null.")); - return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames); - } - - /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. - * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use - * @param name2 - * alternate name - * @param name3 - * alternate name - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3) { - final Set<String> otherNames = new HashSet<>(); - otherNames.add(Objects.requireNonNull(name2, "name2 must not be null.")); - otherNames.add(Objects.requireNonNull(name3, "name3 must not be null.")); - return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames); + new HashSet<>(Objects.requireNonNull(otherNames, + "otherNames must not be null."))); } - + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use - * @param name2 - * alternate name - * @param name3 - * alternate name - * @param name4 - * alternate name + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ - public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3, - final String name4) { - final Set<String> otherNames = new HashSet<>(); - otherNames.add(Objects.requireNonNull(name2, "name2 must not be null.")); - otherNames.add(Objects.requireNonNull(name3, "name3 must not be null.")); - otherNames.add(Objects.requireNonNull(name4, "name4 must not be null.")); - return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames); + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); } - + /** - * Gets a {@code NameSymbol} with a primary name, no symbol, and no other names. + * Gets a {@code NameSymbol} with a primary name, no symbol, and no other + * names. * - * @param name - * name to use + * @param name name to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if name is null + * @throws NullPointerException if name is null */ public static final NameSymbol ofName(final String name) { - return new NameSymbol(Optional.of(name), Optional.empty(), new HashSet<>()); + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); } - + /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. * <p> - * If any argument is null, this static factory replaces it with an empty Optional or empty Set. + * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-11-26 */ - public static final NameSymbol ofNullable(final String name, final String symbol, final Set<String> otherNames) { - return new NameSymbol(Optional.ofNullable(name), Optional.ofNullable(symbol), + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set<String> otherNames) { + return NameSymbol.create(name, symbol, otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); } - + /** - * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional names. + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. * <p> - * If any argument is null, this static factory replaces it with an empty Optional or empty Set. + * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). * - * @param name - * name to use - * @param symbol - * symbol to use - * @param otherNames - * other names to use + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use * @return NameSymbol instance * @since 2019-11-26 */ - public static final NameSymbol ofNullable(final String name, final String symbol, final String... otherNames) { - return new NameSymbol(Optional.ofNullable(name), Optional.ofNullable(symbol), - otherNames == null ? new HashSet<>() : new HashSet<>(Arrays.asList(otherNames))); + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); } - + /** * Gets a {@code NameSymbol} with a symbol and no names. * - * @param symbol - * symbol to use + * @param symbol symbol to use * @return NameSymbol instance * @since 2019-10-21 - * @throws NullPointerException - * if symbol is null + * @throws NullPointerException if symbol is null */ public static final NameSymbol ofSymbol(final String symbol) { - return new NameSymbol(Optional.empty(), Optional.of(symbol), new HashSet<>()); + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); } - + private final Optional<String> primaryName; private final Optional<String> symbol; - + private final Set<String> otherNames; - + /** * Creates the {@code NameSymbol}. * - * @param primaryName - * primary name of unit - * @param symbol - * symbol used to represent unit - * @param otherNames - * other names and/or spellings + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument * @since 2019-10-21 */ - private NameSymbol(final Optional<String> primaryName, final Optional<String> symbol, - final Set<String> otherNames) { + private NameSymbol(final Optional<String> primaryName, + final Optional<String> symbol, final Set<String> otherNames) { this.primaryName = primaryName; this.symbol = symbol; + otherNames.remove(null); this.otherNames = Collections.unmodifiableSet(otherNames); + + if (this.primaryName.isEmpty()) { + assert this.otherNames.isEmpty(); + } } - + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!this.otherNames.equals(other.otherNames)) + return false; + if (this.primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!this.primaryName.equals(other.primaryName)) + return false; + if (this.symbol == null) { + if (other.symbol != null) + return false; + } else if (!this.symbol.equals(other.symbol)) + return false; + return true; + } + /** * @return otherNames * @since 2019-10-21 @@ -258,7 +240,7 @@ public final class NameSymbol { public final Set<String> getOtherNames() { return this.otherNames; } - + /** * @return primaryName * @since 2019-10-21 @@ -266,7 +248,7 @@ public final class NameSymbol { public final Optional<String> getPrimaryName() { return this.primaryName; } - + /** * @return symbol * @since 2019-10-21 @@ -274,4 +256,25 @@ public final class NameSymbol { public final Optional<String> getSymbol() { return this.symbol; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + return this.primaryName.isEmpty() && this.symbol.isEmpty(); + } }
\ No newline at end of file diff --git a/src/org/unitConverter/unit/Nameable.java b/src/org/unitConverter/unit/Nameable.java new file mode 100644 index 0000000..36740ab --- /dev/null +++ b/src/org/unitConverter/unit/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set<String> getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional<String> getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional<String> getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java index 0a1bb9b..c88d2bc 100644 --- a/src/org/unitConverter/unit/SI.java +++ b/src/org/unitConverter/unit/SI.java @@ -16,13 +16,17 @@ */
package org.unitConverter.unit;
+import java.util.Set;
+
import org.unitConverter.math.ObjectProduct;
/**
- * All of the units, prefixes and dimensions that are used by the SI, as well as some outside the SI.
+ * All of the units, prefixes and dimensions that are used by the SI, as well as
+ * some outside the SI.
*
* <p>
- * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}:
+ * This class does not include prefixed units. To obtain prefixed units, use
+ * {@link LinearUnit#withPrefix}:
*
* <pre>
* LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
@@ -36,42 +40,64 @@ public final class SI { /// dimensions used by SI units
// base dimensions, as BaseDimensions
public static final class BaseDimensions {
- public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", "L");
- public static final BaseDimension MASS = BaseDimension.valueOf("Mass", "M");
- public static final BaseDimension TIME = BaseDimension.valueOf("Time", "T");
- public static final BaseDimension ELECTRIC_CURRENT = BaseDimension.valueOf("Electric Current", "I");
- public static final BaseDimension TEMPERATURE = BaseDimension.valueOf("Temperature", "\u0398"); // theta symbol
- public static final BaseDimension QUANTITY = BaseDimension.valueOf("Quantity", "N");
- public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension.valueOf("Luminous Intensity", "J");
- public static final BaseDimension INFORMATION = BaseDimension.valueOf("Information", "Info"); // non-SI
- public static final BaseDimension CURRENCY = BaseDimension.valueOf("Currency", "$$"); // non-SI
-
+ public static final BaseDimension LENGTH = BaseDimension.valueOf("Length",
+ "L");
+ public static final BaseDimension MASS = BaseDimension.valueOf("Mass",
+ "M");
+ public static final BaseDimension TIME = BaseDimension.valueOf("Time",
+ "T");
+ public static final BaseDimension ELECTRIC_CURRENT = BaseDimension
+ .valueOf("Electric Current", "I");
+ public static final BaseDimension TEMPERATURE = BaseDimension
+ .valueOf("Temperature", "\u0398"); // theta symbol
+ public static final BaseDimension QUANTITY = BaseDimension
+ .valueOf("Quantity", "N");
+ public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension
+ .valueOf("Luminous Intensity", "J");
+ public static final BaseDimension INFORMATION = BaseDimension
+ .valueOf("Information", "Info"); // non-SI
+ public static final BaseDimension CURRENCY = BaseDimension
+ .valueOf("Currency", "$$"); // non-SI
+
// You may NOT get SI.BaseDimensions instances!
private BaseDimensions() {
throw new AssertionError();
}
}
-
+
/// base units of the SI
- // suppressing warnings since these are the same object, but in a different form (class)
+ // suppressing warnings since these are the same object, but in a different
+ /// form (class)
@SuppressWarnings("hiding")
public static final class BaseUnits {
- public static final BaseUnit METRE = BaseUnit.valueOf(BaseDimensions.LENGTH, "metre", "m");
- public static final BaseUnit KILOGRAM = BaseUnit.valueOf(BaseDimensions.MASS, "kilogram", "kg");
- public static final BaseUnit SECOND = BaseUnit.valueOf(BaseDimensions.TIME, "second", "s");
- public static final BaseUnit AMPERE = BaseUnit.valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
- public static final BaseUnit KELVIN = BaseUnit.valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
- public static final BaseUnit MOLE = BaseUnit.valueOf(BaseDimensions.QUANTITY, "mole", "mol");
- public static final BaseUnit CANDELA = BaseUnit.valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
- public static final BaseUnit BIT = BaseUnit.valueOf(BaseDimensions.INFORMATION, "bit", "b");
- public static final BaseUnit DOLLAR = BaseUnit.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
-
+ public static final BaseUnit METRE = BaseUnit
+ .valueOf(BaseDimensions.LENGTH, "metre", "m");
+ public static final BaseUnit KILOGRAM = BaseUnit
+ .valueOf(BaseDimensions.MASS, "kilogram", "kg");
+ public static final BaseUnit SECOND = BaseUnit
+ .valueOf(BaseDimensions.TIME, "second", "s");
+ public static final BaseUnit AMPERE = BaseUnit
+ .valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
+ public static final BaseUnit KELVIN = BaseUnit
+ .valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
+ public static final BaseUnit MOLE = BaseUnit
+ .valueOf(BaseDimensions.QUANTITY, "mole", "mol");
+ public static final BaseUnit CANDELA = BaseUnit
+ .valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
+ public static final BaseUnit BIT = BaseUnit
+ .valueOf(BaseDimensions.INFORMATION, "bit", "b");
+ public static final BaseUnit DOLLAR = BaseUnit
+ .valueOf(BaseDimensions.CURRENCY, "dollar", "$");
+
+ public static final Set<BaseUnit> BASE_UNITS = Set.of(METRE, KILOGRAM,
+ SECOND, AMPERE, KELVIN, MOLE, CANDELA, BIT);
+
// You may NOT get SI.BaseUnits instances!
private BaseUnits() {
throw new AssertionError();
}
}
-
+
/**
* Constants that relate to the SI or other systems.
*
@@ -79,195 +105,308 @@ public final class SI { * @since 2019-11-08
*/
public static final class Constants {
- public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND).dividedBy(SECOND).times(9.80665);
+ public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND)
+ .dividedBy(SECOND).times(9.80665);
}
-
+
// dimensions used in the SI, as ObjectProducts
public static final class Dimensions {
- public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct.empty();
- public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH);
- public static final ObjectProduct<BaseDimension> MASS = ObjectProduct.oneOf(BaseDimensions.MASS);
- public static final ObjectProduct<BaseDimension> TIME = ObjectProduct.oneOf(BaseDimensions.TIME);
+ public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct
+ .empty();
+ public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct
+ .oneOf(BaseDimensions.LENGTH);
+ public static final ObjectProduct<BaseDimension> MASS = ObjectProduct
+ .oneOf(BaseDimensions.MASS);
+ public static final ObjectProduct<BaseDimension> TIME = ObjectProduct
+ .oneOf(BaseDimensions.TIME);
public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct
.oneOf(BaseDimensions.ELECTRIC_CURRENT);
- public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE);
- public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY);
+ public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct
+ .oneOf(BaseDimensions.TEMPERATURE);
+ public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct
+ .oneOf(BaseDimensions.QUANTITY);
public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct
.oneOf(BaseDimensions.LUMINOUS_INTENSITY);
- public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION);
- public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY);
+ public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct
+ .oneOf(BaseDimensions.INFORMATION);
+ public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct
+ .oneOf(BaseDimensions.CURRENCY);
+
// derived dimensions without named SI units
- public static final ObjectProduct<BaseDimension> AREA = LENGTH.times(LENGTH);
-
- public static final ObjectProduct<BaseDimension> VOLUME = AREA.times(LENGTH);
- public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY.dividedBy(LENGTH);
- public static final ObjectProduct<BaseDimension> MASS_DENSITY = MASS.dividedBy(VOLUME);
- public static final ObjectProduct<BaseDimension> SURFACE_DENSITY = MASS.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
- public static final ObjectProduct<BaseDimension> CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
- public static final ObjectProduct<BaseDimension> CONCENTRATION = QUANTITY.dividedBy(VOLUME);
- public static final ObjectProduct<BaseDimension> MASS_CONCENTRATION = CONCENTRATION.times(MASS);
- public static final ObjectProduct<BaseDimension> LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
- public static final ObjectProduct<BaseDimension> REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
- public static final ObjectProduct<BaseDimension> ANGLE = LENGTH.dividedBy(LENGTH);
- public static final ObjectProduct<BaseDimension> SOLID_ANGLE = AREA.dividedBy(AREA);
-
+ public static final ObjectProduct<BaseDimension> AREA = LENGTH
+ .times(LENGTH);
+ public static final ObjectProduct<BaseDimension> VOLUME = AREA
+ .times(LENGTH);
+ public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY
+ .dividedBy(LENGTH);
+ public static final ObjectProduct<BaseDimension> MASS_DENSITY = MASS
+ .dividedBy(VOLUME);
+ public static final ObjectProduct<BaseDimension> SURFACE_DENSITY = MASS
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> SPECIFIC_VOLUME = VOLUME
+ .dividedBy(MASS);
+ public static final ObjectProduct<BaseDimension> CURRENT_DENSITY = ELECTRIC_CURRENT
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT
+ .dividedBy(LENGTH);
+ public static final ObjectProduct<BaseDimension> CONCENTRATION = QUANTITY
+ .dividedBy(VOLUME);
+ public static final ObjectProduct<BaseDimension> MASS_CONCENTRATION = CONCENTRATION
+ .times(MASS);
+ public static final ObjectProduct<BaseDimension> LUMINANCE = LUMINOUS_INTENSITY
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> REFRACTIVE_INDEX = VELOCITY
+ .dividedBy(VELOCITY);
+ public static final ObjectProduct<BaseDimension> REFRACTIVE_PERMEABILITY = EMPTY
+ .times(EMPTY);
+ public static final ObjectProduct<BaseDimension> ANGLE = LENGTH
+ .dividedBy(LENGTH);
+ public static final ObjectProduct<BaseDimension> SOLID_ANGLE = AREA
+ .dividedBy(AREA);
+
// derived dimensions with named SI units
- public static final ObjectProduct<BaseDimension> FREQUENCY = EMPTY.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> FORCE = MASS.times(ACCELERATION);
- public static final ObjectProduct<BaseDimension> ENERGY = FORCE.times(LENGTH);
- public static final ObjectProduct<BaseDimension> POWER = ENERGY.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
- public static final ObjectProduct<BaseDimension> VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
- public static final ObjectProduct<BaseDimension> CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
- public static final ObjectProduct<BaseDimension> ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
- public static final ObjectProduct<BaseDimension> ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
- public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX = VOLTAGE.times(TIME);
- public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
- public static final ObjectProduct<BaseDimension> LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
- public static final ObjectProduct<BaseDimension> ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
- public static final ObjectProduct<BaseDimension> CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
-
+ public static final ObjectProduct<BaseDimension> FREQUENCY = EMPTY
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> FORCE = MASS
+ .times(ACCELERATION);
+ public static final ObjectProduct<BaseDimension> ENERGY = FORCE
+ .times(LENGTH);
+ public static final ObjectProduct<BaseDimension> POWER = ENERGY
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> ELECTRIC_CHARGE = ELECTRIC_CURRENT
+ .times(TIME);
+ public static final ObjectProduct<BaseDimension> VOLTAGE = ENERGY
+ .dividedBy(ELECTRIC_CHARGE);
+ public static final ObjectProduct<BaseDimension> CAPACITANCE = ELECTRIC_CHARGE
+ .dividedBy(VOLTAGE);
+ public static final ObjectProduct<BaseDimension> ELECTRIC_RESISTANCE = VOLTAGE
+ .dividedBy(ELECTRIC_CURRENT);
+ public static final ObjectProduct<BaseDimension> ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT
+ .dividedBy(VOLTAGE);
+ public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX = VOLTAGE
+ .times(TIME);
+ public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> INDUCTANCE = MAGNETIC_FLUX
+ .dividedBy(ELECTRIC_CURRENT);
+ public static final ObjectProduct<BaseDimension> LUMINOUS_FLUX = LUMINOUS_INTENSITY
+ .times(SOLID_ANGLE);
+ public static final ObjectProduct<BaseDimension> ILLUMINANCE = LUMINOUS_FLUX
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> SPECIFIC_ENERGY = ENERGY
+ .dividedBy(MASS);
+ public static final ObjectProduct<BaseDimension> CATALYTIC_ACTIVITY = QUANTITY
+ .dividedBy(TIME);
+
// You may NOT get SI.Dimension instances!
private Dimensions() {
throw new AssertionError();
}
}
-
+
/// The units of the SI
- public static final LinearUnit ONE = LinearUnit.valueOf(ObjectProduct.empty(), 1);
+ public static final LinearUnit ONE = LinearUnit
+ .valueOf(ObjectProduct.empty(), 1);
+
public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit()
.withName(NameSymbol.of("metre", "m", "meter"));
public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit()
.withName(NameSymbol.of("kilogram", "kg"));
public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit()
.withName(NameSymbol.of("second", "s", "sec"));
- public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit().withName(NameSymbol.of("ampere", "A"));
- public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit().withName(NameSymbol.of("kelvin", "K"));
- public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit().withName(NameSymbol.of("mole", "mol"));
- public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit().withName(NameSymbol.of("candela", "cd"));
- public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit().withName(NameSymbol.of("bit", "b"));
- public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit().withName(NameSymbol.of("dollar", "$"));
-
+ public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit()
+ .withName(NameSymbol.of("ampere", "A"));
+ public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit()
+ .withName(NameSymbol.of("kelvin", "K"));
+ public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit()
+ .withName(NameSymbol.of("mole", "mol"));
+ public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit()
+ .withName(NameSymbol.of("candela", "cd"));
+ public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit()
+ .withName(NameSymbol.of("bit", "b"));
+ public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit()
+ .withName(NameSymbol.of("dollar", "$"));
// Non-base units
- public static final LinearUnit RADIAN = METRE.dividedBy(METRE).withName(NameSymbol.of("radian", "rad"));
- public static final LinearUnit STERADIAN = RADIAN.times(RADIAN).withName(NameSymbol.of("steradian", "sr"));
- public static final LinearUnit HERTZ = ONE.dividedBy(SECOND).withName(NameSymbol.of("hertz", "Hz"));
+ public static final LinearUnit RADIAN = METRE.dividedBy(METRE)
+ .withName(NameSymbol.of("radian", "rad"));
+
+ public static final LinearUnit STERADIAN = RADIAN.times(RADIAN)
+ .withName(NameSymbol.of("steradian", "sr"));
+ public static final LinearUnit HERTZ = ONE.dividedBy(SECOND)
+ .withName(NameSymbol.of("hertz", "Hz"));
// for periodic phenomena
- public static final LinearUnit NEWTON = KILOGRAM.times(METRE).dividedBy(SECOND.times(SECOND))
+ public static final LinearUnit NEWTON = KILOGRAM.times(METRE)
+ .dividedBy(SECOND.times(SECOND))
.withName(NameSymbol.of("newton", "N"));
public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE))
.withName(NameSymbol.of("pascal", "Pa"));
- public static final LinearUnit JOULE = NEWTON.times(METRE).withName(NameSymbol.of("joule", "J"));
- public static final LinearUnit WATT = JOULE.dividedBy(SECOND).withName(NameSymbol.of("watt", "W"));
- public static final LinearUnit COULOMB = AMPERE.times(SECOND).withName(NameSymbol.of("coulomb", "C"));
- public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB).withName(NameSymbol.of("volt", "V"));
- public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT).withName(NameSymbol.of("farad", "F"));
- public static final LinearUnit OHM = VOLT.dividedBy(AMPERE).withName(NameSymbol.of("ohm", "\u03A9")); // omega
- public static final LinearUnit SIEMENS = ONE.dividedBy(OHM).withName(NameSymbol.of("siemens", "S"));
- public static final LinearUnit WEBER = VOLT.times(SECOND).withName(NameSymbol.of("weber", "Wb"));
- public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("tesla", "T"));
- public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE).withName(NameSymbol.of("henry", "H"));
- public static final LinearUnit LUMEN = CANDELA.times(STERADIAN).withName(NameSymbol.of("lumen", "lm"));
- public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("lux", "lx"));
- public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND).withName(NameSymbol.of("bequerel", "Bq"));
+ public static final LinearUnit JOULE = NEWTON.times(METRE)
+ .withName(NameSymbol.of("joule", "J"));
+ public static final LinearUnit WATT = JOULE.dividedBy(SECOND)
+ .withName(NameSymbol.of("watt", "W"));
+ public static final LinearUnit COULOMB = AMPERE.times(SECOND)
+ .withName(NameSymbol.of("coulomb", "C"));
+ public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB)
+ .withName(NameSymbol.of("volt", "V"));
+ public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT)
+ .withName(NameSymbol.of("farad", "F"));
+ public static final LinearUnit OHM = VOLT.dividedBy(AMPERE)
+ .withName(NameSymbol.of("ohm", "\u03A9")); // omega
+ public static final LinearUnit SIEMENS = ONE.dividedBy(OHM)
+ .withName(NameSymbol.of("siemens", "S"));
+ public static final LinearUnit WEBER = VOLT.times(SECOND)
+ .withName(NameSymbol.of("weber", "Wb"));
+ public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE))
+ .withName(NameSymbol.of("tesla", "T"));
+ public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE)
+ .withName(NameSymbol.of("henry", "H"));
+ public static final LinearUnit LUMEN = CANDELA.times(STERADIAN)
+ .withName(NameSymbol.of("lumen", "lm"));
+ public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE))
+ .withName(NameSymbol.of("lux", "lx"));
+ public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND)
+ .withName(NameSymbol.of("bequerel", "Bq"));
// for activity referred to a nucleotide
- public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("grey", "Gy"));
+ public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM)
+ .withName(NameSymbol.of("grey", "Gy"));
// for absorbed dose
- public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("sievert", "Sv"));
+ public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM)
+ .withName(NameSymbol.of("sievert", "Sv"));
// for dose equivalent
- public static final LinearUnit KATAL = MOLE.dividedBy(SECOND).withName(NameSymbol.of("katal", "kat"));
-
+ public static final LinearUnit KATAL = MOLE.dividedBy(SECOND)
+ .withName(NameSymbol.of("katal", "kat"));
// common derived units included for convenience
- public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000).withName(NameSymbol.of("gram", "g"));
+ public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000)
+ .withName(NameSymbol.of("gram", "g"));
+
public static final LinearUnit SQUARE_METRE = METRE.toExponent(2)
- .withName(NameSymbol.of("square metre", "m^2", "square meter", "metre squared", "meter squared"));
+ .withName(NameSymbol.of("square metre", "m^2", "square meter",
+ "metre squared", "meter squared"));
public static final LinearUnit CUBIC_METRE = METRE.toExponent(3)
- .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", "metre cubed", "meter cubed"));
+ .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter",
+ "metre cubed", "meter cubed"));
public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND)
- .withName(NameSymbol.of("metre per second", "m/s", "meter per second"));
-
+ .withName(
+ NameSymbol.of("metre per second", "m/s", "meter per second"));
// Non-SI units included for convenience
public static final Unit CELSIUS = Unit
- .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15, tempC -> tempC + 273.15)
+ .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15,
+ tempC -> tempC + 273.15)
.withName(NameSymbol.of("degree Celsius", "\u00B0C"));
- public static final LinearUnit MINUTE = SECOND.times(60).withName(NameSymbol.of("minute", "min"));
- public static final LinearUnit HOUR = MINUTE.times(60).withName(NameSymbol.of("hour", "h", "hr"));
- public static final LinearUnit DAY = HOUR.times(60).withName(NameSymbol.of("day", "d"));
- public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000).dividedBy(HOUR)
- .withName(NameSymbol.of("kilometre per hour", "km/h", "kilometer per hour"));
+
+ public static final LinearUnit MINUTE = SECOND.times(60)
+ .withName(NameSymbol.of("minute", "min"));
+ public static final LinearUnit HOUR = MINUTE.times(60)
+ .withName(NameSymbol.of("hour", "h", "hr"));
+ public static final LinearUnit DAY = HOUR.times(60)
+ .withName(NameSymbol.of("day", "d"));
+ public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000)
+ .dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h",
+ "kilometer per hour"));
public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI))
.withName(NameSymbol.of("degree", "\u00B0", "deg"));
- public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60).withName(NameSymbol.of("arcminute", "arcmin"));
- public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60).withName(NameSymbol.of("arcsecond", "arcsec"));
- public static final LinearUnit ASTRONOMICAL_UNIT = METRE.times(149597870700.0)
+ public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60)
+ .withName(NameSymbol.of("arcminute", "arcmin"));
+ public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60)
+ .withName(NameSymbol.of("arcsecond", "arcsec"));
+ public static final LinearUnit ASTRONOMICAL_UNIT = METRE
+ .times(149597870700.0)
.withName(NameSymbol.of("astronomical unit", "au"));
- public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT.dividedBy(ARCSECOND)
- .withName(NameSymbol.of("parsec", "pc"));
- public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0).withName(NameSymbol.of("hectare", "ha"));
- public static final LinearUnit LITRE = METRE.times(METRE).times(METRE).dividedBy(1000.0)
- .withName(NameSymbol.of("litre", "L", "l", "liter"));
- public static final LinearUnit TONNE = KILOGRAM.times(1000.0).withName(NameSymbol.of("tonne", "t", "metric ton"));
+ public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT
+ .dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc"));
+ public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0)
+ .withName(NameSymbol.of("hectare", "ha"));
+ public static final LinearUnit LITRE = METRE.times(METRE).times(METRE)
+ .dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter"));
+ public static final LinearUnit TONNE = KILOGRAM.times(1000.0)
+ .withName(NameSymbol.of("tonne", "t", "metric ton"));
public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27)
- .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate value
+ .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate
+ // value
public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19)
.withName(NameSymbol.of("electron volt", "eV"));
- public static final LinearUnit BYTE = BIT.times(8).withName(NameSymbol.of("byte", "B"));
- public static final Unit NEPER = Unit
- .fromConversionFunctions(ONE.getBase(), pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
+ public static final LinearUnit BYTE = BIT.times(8)
+ .withName(NameSymbol.of("byte", "B"));
+ public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(),
+ pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
.withName(NameSymbol.of("neper", "Np"));
- public static final Unit BEL = Unit
- .fromConversionFunctions(ONE.getBase(), pr -> Math.log10(pr), dB -> Math.pow(10, dB))
+ public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(),
+ pr -> Math.log10(pr), dB -> Math.pow(10, dB))
.withName(NameSymbol.of("bel", "B"));
public static final Unit DECIBEL = Unit
- .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), dB -> Math.pow(10, dB / 10))
+ .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr),
+ dB -> Math.pow(10, dB / 10))
.withName(NameSymbol.of("decibel", "dB"));
-
+
/// The prefixes of the SI
// expanding decimal prefixes
- public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3).withName(NameSymbol.of("kilo", "k", "K"));
- public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6).withName(NameSymbol.of("mega", "M"));
- public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9).withName(NameSymbol.of("giga", "G"));
- public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12).withName(NameSymbol.of("tera", "T"));
- public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15).withName(NameSymbol.of("peta", "P"));
- public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18).withName(NameSymbol.of("exa", "E"));
- public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21).withName(NameSymbol.of("zetta", "Z"));
- public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24).withName(NameSymbol.of("yotta", "Y"));
-
+ public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3)
+ .withName(NameSymbol.of("kilo", "k", "K"));
+ public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6)
+ .withName(NameSymbol.of("mega", "M"));
+ public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9)
+ .withName(NameSymbol.of("giga", "G"));
+ public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12)
+ .withName(NameSymbol.of("tera", "T"));
+ public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15)
+ .withName(NameSymbol.of("peta", "P"));
+ public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18)
+ .withName(NameSymbol.of("exa", "E"));
+ public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21)
+ .withName(NameSymbol.of("zetta", "Z"));
+ public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24)
+ .withName(NameSymbol.of("yotta", "Y"));
+
// contracting decimal prefixes
- public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3).withName(NameSymbol.of("milli", "m"));
- public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6).withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
- public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9).withName(NameSymbol.of("nano", "n"));
- public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12).withName(NameSymbol.of("pico", "p"));
- public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15).withName(NameSymbol.of("femto", "f"));
- public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18).withName(NameSymbol.of("atto", "a"));
- public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21).withName(NameSymbol.of("zepto", "z"));
- public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24).withName(NameSymbol.of("yocto", "y"));
-
+ public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3)
+ .withName(NameSymbol.of("milli", "m"));
+ public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6)
+ .withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
+ public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9)
+ .withName(NameSymbol.of("nano", "n"));
+ public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12)
+ .withName(NameSymbol.of("pico", "p"));
+ public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15)
+ .withName(NameSymbol.of("femto", "f"));
+ public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18)
+ .withName(NameSymbol.of("atto", "a"));
+ public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21)
+ .withName(NameSymbol.of("zepto", "z"));
+ public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24)
+ .withName(NameSymbol.of("yocto", "y"));
+
// prefixes that don't match the pattern of thousands
- public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1).withName(NameSymbol.of("deka", "da", "deca", "D"));
- public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2).withName(NameSymbol.of("hecto", "h", "H", "hekto"));
- public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1).withName(NameSymbol.of("deci", "d"));
- public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2).withName(NameSymbol.of("centi", "c"));
- public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024).withName(NameSymbol.of("kibi", "Ki"));
- public static final UnitPrefix MEBI = KIBI.times(1024).withName(NameSymbol.of("mebi", "Mi"));
- public static final UnitPrefix GIBI = MEBI.times(1024).withName(NameSymbol.of("gibi", "Gi"));
- public static final UnitPrefix TEBI = GIBI.times(1024).withName(NameSymbol.of("tebi", "Ti"));
- public static final UnitPrefix PEBI = TEBI.times(1024).withName(NameSymbol.of("pebi", "Pi"));
- public static final UnitPrefix EXBI = PEBI.times(1024).withName(NameSymbol.of("exbi", "Ei"));
-
+ public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1)
+ .withName(NameSymbol.of("deka", "da", "deca", "D"));
+ public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2)
+ .withName(NameSymbol.of("hecto", "h", "H", "hekto"));
+ public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1)
+ .withName(NameSymbol.of("deci", "d"));
+ public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2)
+ .withName(NameSymbol.of("centi", "c"));
+ public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024)
+ .withName(NameSymbol.of("kibi", "Ki"));
+ public static final UnitPrefix MEBI = KIBI.times(1024)
+ .withName(NameSymbol.of("mebi", "Mi"));
+ public static final UnitPrefix GIBI = MEBI.times(1024)
+ .withName(NameSymbol.of("gibi", "Gi"));
+ public static final UnitPrefix TEBI = GIBI.times(1024)
+ .withName(NameSymbol.of("tebi", "Ti"));
+ public static final UnitPrefix PEBI = TEBI.times(1024)
+ .withName(NameSymbol.of("pebi", "Pi"));
+ public static final UnitPrefix EXBI = PEBI.times(1024)
+ .withName(NameSymbol.of("exbi", "Ei"));
+
// a few prefixed units
public static final LinearUnit MICROMETRE = SI.METRE.withPrefix(SI.MICRO);
public static final LinearUnit MILLIMETRE = SI.METRE.withPrefix(SI.MILLI);
public static final LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
public static final LinearUnit MEGAMETRE = SI.METRE.withPrefix(SI.MEGA);
-
+
public static final LinearUnit MICROLITRE = SI.LITRE.withPrefix(SI.MICRO);
public static final LinearUnit MILLILITRE = SI.LITRE.withPrefix(SI.MILLI);
public static final LinearUnit KILOLITRE = SI.LITRE.withPrefix(SI.KILO);
@@ -291,27 +430,48 @@ public final class SI { public static final LinearUnit MILLIJOULE = SI.JOULE.withPrefix(SI.MILLI);
public static final LinearUnit KILOJOULE = SI.JOULE.withPrefix(SI.KILO);
public static final LinearUnit MEGAJOULE = SI.JOULE.withPrefix(SI.MEGA);
-
+
public static final LinearUnit MICROWATT = SI.WATT.withPrefix(SI.MICRO);
public static final LinearUnit MILLIWATT = SI.WATT.withPrefix(SI.MILLI);
public static final LinearUnit KILOWATT = SI.WATT.withPrefix(SI.KILO);
public static final LinearUnit MEGAWATT = SI.WATT.withPrefix(SI.MEGA);
- public static final LinearUnit MICROCOULOMB = SI.COULOMB.withPrefix(SI.MICRO);
- public static final LinearUnit MILLICOULOMB = SI.COULOMB.withPrefix(SI.MILLI);
+ public static final LinearUnit MICROCOULOMB = SI.COULOMB
+ .withPrefix(SI.MICRO);
+ public static final LinearUnit MILLICOULOMB = SI.COULOMB
+ .withPrefix(SI.MILLI);
public static final LinearUnit KILOCOULOMB = SI.COULOMB.withPrefix(SI.KILO);
public static final LinearUnit MEGACOULOMB = SI.COULOMB.withPrefix(SI.MEGA);
-
+
public static final LinearUnit MICROAMPERE = SI.AMPERE.withPrefix(SI.MICRO);
public static final LinearUnit MILLIAMPERE = SI.AMPERE.withPrefix(SI.MILLI);
-
+
public static final LinearUnit MICROVOLT = SI.VOLT.withPrefix(SI.MICRO);
public static final LinearUnit MILLIVOLT = SI.VOLT.withPrefix(SI.MILLI);
public static final LinearUnit KILOVOLT = SI.VOLT.withPrefix(SI.KILO);
public static final LinearUnit MEGAVOLT = SI.VOLT.withPrefix(SI.MEGA);
-
+
public static final LinearUnit KILOOHM = SI.OHM.withPrefix(SI.KILO);
public static final LinearUnit MEGAOHM = SI.OHM.withPrefix(SI.MEGA);
+
+ // sets of prefixes
+ public static final Set<UnitPrefix> ALL_PREFIXES = Set.of(DEKA, HECTO, KILO,
+ MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, MICRO,
+ NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI,
+ EXBI);
+
+ public static final Set<UnitPrefix> DECIMAL_PREFIXES = Set.of(DEKA, HECTO,
+ KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI,
+ MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO);
+ public static final Set<UnitPrefix> THOUSAND_PREFIXES = Set.of(KILO, MEGA,
+ GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO,
+ ATTO, ZEPTO, YOCTO);
+ public static final Set<UnitPrefix> MAGNIFYING_PREFIXES = Set.of(DEKA, HECTO,
+ KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI,
+ TEBI, PEBI, EXBI);
+ public static final Set<UnitPrefix> REDUCING_PREFIXES = Set.of(DECI, CENTI,
+ MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO);
+
// You may NOT get SI instances!
private SI() {
throw new AssertionError();
diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index 35b32fc..0a3298f 100644 --- a/src/org/unitConverter/unit/Unit.java +++ b/src/org/unitConverter/unit/Unit.java @@ -16,15 +16,14 @@ */ package org.unitConverter.unit; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.DoubleUnaryOperator; +import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ObjectProduct; /** @@ -33,211 +32,252 @@ import org.unitConverter.math.ObjectProduct; * @author Adrien Hopkins * @since 2019-10-16 */ -public abstract class Unit { +public abstract class Unit implements Nameable { /** - * Returns a unit from its base and the functions it uses to convert to and from its base. + * Returns a unit from its base and the functions it uses to convert to and + * from its base. * * <p> - * For example, to get a unit representing the degree Celsius, the following code can be used: + * For example, to get a unit representing the degree Celsius, the following + * code can be used: * * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} * </p> * - * @param base - * unit's base - * @param converterFrom - * function that accepts a value expressed in the unit's base and returns that value expressed in this - * unit. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. + * @param base unit's base + * @param converterFrom function that accepts a value expressed in the unit's + * base and returns that value expressed in this unit. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. * @return a unit that uses the provided functions to convert. * @since 2019-05-22 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ - public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base, - final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { return new FunctionalUnit(base, converterFrom, converterTo); } - + /** - * Returns a unit from its base and the functions it uses to convert to and from its base. + * Returns a unit from its base and the functions it uses to convert to and + * from its base. * * <p> - * For example, to get a unit representing the degree Celsius, the following code can be used: + * For example, to get a unit representing the degree Celsius, the following + * code can be used: * * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} * </p> * - * @param base - * unit's base - * @param converterFrom - * function that accepts a value expressed in the unit's base and returns that value expressed in this - * unit. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. - * @param ns - * names and symbol of unit + * @param base unit's base + * @param converterFrom function that accepts a value expressed in the unit's + * base and returns that value expressed in this unit. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @param ns names and symbol of unit * @return a unit that uses the provided functions to convert. * @since 2019-05-22 - * @throws NullPointerException - * if any argument is null + * @throws NullPointerException if any argument is null */ - public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base, - final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo, final NameSymbol ns) { + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { return new FunctionalUnit(base, converterFrom, converterTo, ns); } - + /** * The combination of units that this unit is based on. * * @since 2019-10-16 */ private final ObjectProduct<BaseUnit> unitBase; - - /** - * The primary name used by this unit. - */ - private final Optional<String> primaryName; - + /** - * A short symbol used to represent this unit. - */ - private final Optional<String> symbol; - - /** - * A set of any additional names and/or spellings that the unit uses. + * This unit's name(s) and symbol + * + * @since 2020-09-07 */ - private final Set<String> otherNames; - + private final NameSymbol nameSymbol; + /** * Cache storing the result of getDimension() * * @since 2019-10-16 */ private transient ObjectProduct<BaseDimension> dimension = null; - + /** - * Creates the {@code AbstractUnit}. + * Creates the {@code Unit}. * - * @param unitBase - * base of unit - * @param ns - * names and symbol of unit + * @param unitBase base of unit + * @param ns names and symbol of unit * @since 2019-10-16 - * @throws NullPointerException - * if unitBase or ns is null + * @throws NullPointerException if unitBase or ns is null */ - protected Unit(final ObjectProduct<BaseUnit> unitBase, final NameSymbol ns) { - this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null."); - this.primaryName = Objects.requireNonNull(ns, "ns must not be null.").getPrimaryName(); - this.symbol = ns.getSymbol(); - this.otherNames = ns.getOtherNames(); + Unit(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); } - + /** * A constructor that constructs {@code BaseUnit} instances. * * @since 2019-10-16 */ - Unit(final String primaryName, final String symbol, final Set<String> otherNames) { + Unit(final String primaryName, final String symbol, + final Set<String> otherNames) { if (this instanceof BaseUnit) { this.unitBase = ObjectProduct.oneOf((BaseUnit) this); } else throw new AssertionError(); - this.primaryName = Optional.of(primaryName); - this.symbol = Optional.of(symbol); - this.otherNames = Collections.unmodifiableSet( - new HashSet<>(Objects.requireNonNull(otherNames, "additionalNames must not be null."))); + this.nameSymbol = NameSymbol.of(primaryName, symbol, + new HashSet<>(otherNames)); } - + /** - * Checks if a value expressed in this unit can be converted to a value expressed in {@code other} + * @return this unit as a {@link Unitlike} + * @since 2020-09-07 + */ + public final Unitlike<Double> asUnitlike() { + return Unitlike.fromConversionFunctions(this.getBase(), + this::convertFromBase, this::convertToBase, this.getNameSymbol()); + } + + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} * - * @param other - * unit to test with - * @return true if the units are compatible + * @param other unit or unitlike form to test with + * @return true if they are compatible * @since 2019-01-13 * @since v0.1.0 - * @throws NullPointerException - * if other is null + * @throws NullPointerException if other is null */ public final boolean canConvertTo(final Unit other) { Objects.requireNonNull(other, "other must not be null."); return Objects.equals(this.getBase(), other.getBase()); } - + + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final <W> boolean canConvertTo(final Unitlike<W> other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + /** - * Converts from a value expressed in this unit's base unit to a value expressed in this unit. + * Converts from a value expressed in this unit's base unit to a value + * expressed in this unit. * <p> - * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal - * to {@code value} for any value, ignoring precision loss by roundoff error. + * This must be the inverse of {@code convertToBase}, so + * {@code convertFromBase(convertToBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. * </p> * <p> - * If this unit <i>is</i> a base unit, this method should return {@code value}. + * If this unit <i>is</i> a base unit, this method should return + * {@code value}. * </p> * - * @implSpec This method is used by {@link #convertTo}, and its behaviour affects the behaviour of - * {@code convertTo}. + * @implSpec This method is used by {@link #convertTo}, and its behaviour + * affects the behaviour of {@code convertTo}. * - * @param value - * value expressed in <b>base</b> unit + * @param value value expressed in <b>base</b> unit * @return value expressed in <b>this</b> unit * @since 2018-12-22 * @since v0.1.0 */ protected abstract double convertFromBase(double value); - + /** - * Converts a value expressed in this unit to a value expressed in {@code other}. + * Converts a value expressed in this unit to a value expressed in + * {@code other}. * * @implSpec If unit conversion is possible, this implementation returns - * {@code other.convertFromBase(this.convertToBase(value))}. Therefore, overriding either of those methods - * will change the output of this method. + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. * - * @param other - * unit to convert to - * @param value - * value to convert + * @param other unit to convert to + * @param value value to convert * @return converted value * @since 2019-05-22 - * @throws IllegalArgumentException - * if {@code other} is incompatible for conversion with this unit (as tested by - * {@link Unit#canConvertTo}). - * @throws NullPointerException - * if other is null + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null */ public final double convertTo(final Unit other, final double value) { Objects.requireNonNull(other, "other must not be null."); if (this.canConvertTo(other)) return other.convertFromBase(this.convertToBase(value)); else - throw new IllegalArgumentException(String.format("Cannot convert from %s to %s.", this, other)); + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); } - + + /** + * Converts a value expressed in this unit to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unitlike form to convert to + * @param value value to convert + * @param <W> type of value to convert to + * @return converted value + * @since 2020-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final <W> W convertTo(final Unitlike<W> other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + /** - * Converts from a value expressed in this unit to a value expressed in this unit's base unit. + * Converts from a value expressed in this unit to a value expressed in this + * unit's base unit. * <p> - * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be - * equal to {@code value} for any value, ignoring precision loss by roundoff error. + * This must be the inverse of {@code convertFromBase}, so + * {@code convertToBase(convertFromBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. * </p> * <p> - * If this unit <i>is</i> a base unit, this method should return {@code value}. + * If this unit <i>is</i> a base unit, this method should return + * {@code value}. * </p> * - * @implSpec This method is used by {@link #convertTo}, and its behaviour affects the behaviour of - * {@code convertTo}. + * @implSpec This method is used by {@link #convertTo}, and its behaviour + * affects the behaviour of {@code convertTo}. * - * @param value - * value expressed in <b>this</b> unit + * @param value value expressed in <b>this</b> unit * @return value expressed in <b>base</b> unit * @since 2018-12-22 * @since v0.1.0 */ protected abstract double convertToBase(double value); - + /** * @return combination of units that this unit is based on * @since 2018-12-22 @@ -246,7 +286,7 @@ public abstract class Unit { public final ObjectProduct<BaseUnit> getBase() { return this.unitBase; } - + /** * @return dimension measured by this unit * @since 2018-12-22 @@ -256,58 +296,82 @@ public abstract class Unit { if (this.dimension == null) { final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); - + for (final BaseUnit key : mapping.keySet()) { dimensionMap.put(key.getBaseDimension(), mapping.get(key)); } - + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); } return this.dimension; } - + /** - * @return additionalNames - * @since 2019-10-21 + * @return the nameSymbol + * @since 2020-09-07 */ - public final Set<String> getOtherNames() { - return this.otherNames; - } - - /** - * @return primaryName - * @since 2019-10-21 - */ - public final Optional<String> getPrimaryName() { - return this.primaryName; + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; } - + /** - * @return symbol - * @since 2019-10-21 + * Returns true iff this unit is metric. + * <p> + * "Metric" is defined by three conditions: + * <ul> + * <li>Must be an instance of {@link LinearUnit}.</li> + * <li>Must be based on the SI base units (as determined by getBase())</li> + * <li>The conversion factor must be a power of 10.</li> + * </ul> + * <p> + * Note that this definition excludes some units that many would consider + * "metric", such as the degree Celsius (fails the first condition), + * calories, minutes and hours (fail the third condition). + * <p> + * All SI units (as designated by the BIPM) except the degree Celsius are + * considered "metric" by this definition. + * + * @since 2020-08-27 */ - public final Optional<String> getSymbol() { - return this.symbol; + public final boolean isMetric() { + // first condition - check that it is a linear unit + if (!(this instanceof LinearUnit)) + return false; + final LinearUnit linear = (LinearUnit) this; + + // second condition - check that + for (final BaseUnit b : linear.getBase().getBaseSet()) { + if (!SI.BaseUnits.BASE_UNITS.contains(b)) + return false; + } + + // third condition - check that conversion factor is a power of 10 + return DecimalComparison + .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); } - + @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "") - + ", derived from " + this.getBase().toString(u -> u.getSymbol().get()) - + (this.getOtherNames().isEmpty() ? "" : ", also called " + String.join(", ", this.getOtherNames())); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); } - + /** - * @param ns - * name(s) and symbol to use + * @param ns name(s) and symbol to use * @return a copy of this unit with provided name(s) and symbol * @since 2019-10-21 - * @throws NullPointerException - * if ns is null + * @throws NullPointerException if ns is null */ public Unit withName(final NameSymbol ns) { - return fromConversionFunctions(this.getBase(), this::convertFromBase, this::convertToBase, + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, Objects.requireNonNull(ns, "ns must not be null.")); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 507266d..000acf5 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -16,17 +16,18 @@ */ package org.unitConverter.unit; -import java.io.BufferedReader; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -40,9 +41,11 @@ import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.unitConverter.math.ConditionalExistenceCollections; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ExpressionParser; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. @@ -55,29 +58,33 @@ public final class UnitDatabase { /** * A map for units that allows the use of prefixes. * <p> - * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is - * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the - * maps that were passed in during construction. + * As this map implementation is intended to be used as a sort of "augmented + * view" of a unit and prefix map, it is unmodifiable but instead reflects + * the changes to the maps passed into it. Do not edit this map, instead edit + * the maps that were passed in during construction. * </p> * <p> * The rules for applying prefixes onto units are the following: * <ul> * <li>Prefixes can only be applied to linear units.</li> - * <li>Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if - * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with - * the prefix "A", even though they have the same string.</li> - * <li>Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and - * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li> + * <li>Before attempting to search for prefixes in a unit name, this map will + * first search for a unit name. So, if there are two units, "B" and "AB", + * and a prefix "A", this map will favour the unit "AB" over the unit "B" + * with the prefix "A", even though they have the same string.</li> + * <li>Longer prefixes are preferred to shorter prefixes. So, if you have + * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return + * the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li> * </ul> * </p> * <p> - * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some - * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an + * This map is infinite in size if there is at least one unit and at least + * one prefix. If it is infinite, some operations that only work with finite + * collections, like converting name/entry sets to arrays, will throw an * {@code IllegalStateException}. * </p> * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), {@link #containsValue} and {@link #values()} - * currently ignore prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), + * {@link #containsValue} and {@link #values()} currently ignore prefixes. * </p> * * @author Adrien Hopkins @@ -89,16 +96,19 @@ public final class UnitDatabase { * The class used for entry sets. * * <p> - * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this - * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. + * If the map that created this set is infinite in size (has at least one + * unit and at least one prefix), this set is infinite as well. If this + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. * </p> * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitEntrySet extends AbstractSet<Map.Entry<String, Unit>> { + private static final class PrefixedUnitEntrySet + extends AbstractSet<Map.Entry<String, Unit>> { /** * The entry for this set. * @@ -106,17 +116,16 @@ public final class UnitDatabase { * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntry implements Entry<String, Unit> { + private static final class PrefixedUnitEntry + implements Entry<String, Unit> { private final String key; private final Unit value; - + /** * Creates the {@code PrefixedUnitEntry}. * - * @param key - * key - * @param value - * value + * @param key key + * @param value value * @since 2019-04-14 * @since v0.2.0 */ @@ -124,7 +133,7 @@ public final class UnitDatabase { this.key = key; this.value = value; } - + /** * @since 2019-05-03 */ @@ -136,34 +145,38 @@ public final class UnitDatabase { return Objects.equals(this.getKey(), other.getKey()) && Objects.equals(this.getValue(), other.getValue()); } - + @Override public String getKey() { return this.key; } - + @Override public Unit getValue() { return this.value; } - + /** * @since 2019-05-03 */ @Override public int hashCode() { return (this.getKey() == null ? 0 : this.getKey().hashCode()) - ^ (this.getValue() == null ? 0 : this.getValue().hashCode()); + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); } - + @Override public Unit setValue(final Unit value) { - throw new UnsupportedOperationException("Cannot set value in an immutable entry"); + throw new UnsupportedOperationException( + "Cannot set value in an immutable entry"); } - + /** - * Returns a string representation of the entry. The format of the string is the string representation - * of the key, then the equals ({@code =}) character, then the string representation of the value. + * Returns a string representation of the entry. The format of the + * string is the string representation of the key, then the equals + * ({@code =}) character, then the string representation of the + * value. * * @since 2019-05-03 */ @@ -172,27 +185,30 @@ public final class UnitDatabase { return this.getKey() + "=" + this.getValue(); } } - + /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntryIterator implements Iterator<Entry<String, Unit>> { + private static final class PrefixedUnitEntryIterator + implements Iterator<Entry<String, Unit>> { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List<Integer> prefixCoordinates = new ArrayList<>(); - + // values from the unit entry set private final Map<String, Unit> map; private transient final List<String> unitNames; private transient final List<String> prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -202,7 +218,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -214,10 +230,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -229,7 +245,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -238,127 +254,142 @@ public final class UnitDatabase { */ private void incrementPosition() { this.unitNamePosition++; - + if (this.unitNamePosition >= this.unitNames.size()) { // we have used all of our units, go to a different prefix this.unitNamePosition = 0; - + // if the prefix coordinates are empty, then set it to [0] if (this.prefixCoordinates.isEmpty()) { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + if (i < 0) { // we need to add a new coordinate this.prefixCoordinates.add(0, 0); } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); } } } } } - + @Override public Entry<String, Unit> next() { // get next element final Entry<String, Unit> nextEntry = this.peek(); - + // iterate to next position this.incrementPosition(); - + return nextEntry; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private Entry<String, Unit> peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); - + // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // since all of the unprefixed stuff is done, just remove + // nonlinear units if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { this.unitNames.remove(this.unitNamePosition); } } - + final String nextName = this.getCurrentUnitName(); - + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); } - + /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. * * @since 2019-05-03 */ @Override public String toString() { - return String.format("Iterator iterating over name-unit entries; next value is \"%s\"", + return String.format( + "Iterator iterating over name-unit entries; next value is \"%s\"", this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * - * @param map - * map that created this set + * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitEntrySet(final PrefixedUnitMap map) { this.map = map; } - + @Override public boolean add(final Map.Entry<String, Unit> e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override - public boolean addAll(final Collection<? extends Map.Entry<String, Unit>> c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + public boolean addAll( + final Collection<? extends Map.Entry<String, Unit>> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { // get the entry final Entry<String, Unit> entry; - + try { - // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown. + // This is OK because I'm in a try-catch block, catching the + // exact exception that would be thrown. @SuppressWarnings("unchecked") final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o; entry = tempEntry; } catch (final ClassCastException e) { - throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); + throw new IllegalArgumentException( + "Attempted to test for an entry using a non-entry."); } - - return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue()); + + return this.map.containsKey(entry.getKey()) + && this.map.get(entry.getKey()).equals(entry.getValue()); } - + @Override public boolean containsAll(final Collection<?> c) { for (final Object o : c) @@ -366,37 +397,42 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator<Entry<String, Unit>> iterator() { return new PrefixedUnitEntryIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeAll(final Collection<?> c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override - public boolean removeIf(final Predicate<? super Entry<String, Unit>> filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + public boolean removeIf( + final Predicate<? super Entry<String, Unit>> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection<?> c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -409,10 +445,9 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { @@ -420,12 +455,12 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public <T> T[] toArray(final T[] a) { @@ -433,53 +468,61 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", this.map.units, this.map.prefixes); } } - + /** * The class used for unit name sets. * * <p> - * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this - * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. + * If the map that created this set is infinite in size (has at least one + * unit and at least one prefix), this set is infinite as well. If this + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. * </p> * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitNameSet extends AbstractSet<String> { + private static final class PrefixedUnitNameSet + extends AbstractSet<String> { /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitNameIterator implements Iterator<String> { + private static final class PrefixedUnitNameIterator + implements Iterator<String> { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List<Integer> prefixCoordinates = new ArrayList<>(); - + // values from the unit name set private final Map<String, Unit> map; private transient final List<String> unitNames; private transient final List<String> prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -489,7 +532,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -501,10 +544,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -516,7 +559,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -525,109 +568,121 @@ public final class UnitDatabase { */ private void incrementPosition() { this.unitNamePosition++; - + if (this.unitNamePosition >= this.unitNames.size()) { // we have used all of our units, go to a different prefix this.unitNamePosition = 0; - + // if the prefix coordinates are empty, then set it to [0] if (this.prefixCoordinates.isEmpty()) { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + if (i < 0) { // we need to add a new coordinate this.prefixCoordinates.add(0, 0); } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); } } } } } - + @Override public String next() { final String nextName = this.peek(); - + this.incrementPosition(); - + return nextName; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private String peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // since all of the unprefixed stuff is done, just remove + // nonlinear units if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { this.unitNames.remove(this.unitNamePosition); } } - + return this.getCurrentUnitName(); } - + /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. * * @since 2019-05-03 */ @Override public String toString() { - return String.format("Iterator iterating over unit names; next value is \"%s\"", this.peek()); + return String.format( + "Iterator iterating over unit names; next value is \"%s\"", + this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * - * @param map - * map that created this set + * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitNameSet(final PrefixedUnitMap map) { this.map = map; } - + @Override public boolean add(final String e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public boolean addAll(final Collection<? extends String> c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { return this.map.containsKey(o); } - + @Override public boolean containsAll(final Collection<?> c) { for (final Object o : c) @@ -635,37 +690,41 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator<String> iterator() { return new PrefixedUnitNameIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeAll(final Collection<?> c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeIf(final Predicate<? super String> filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection<?> c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -678,10 +737,9 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { @@ -689,13 +747,13 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public <T> T[] toArray(final T[] a) { @@ -703,19 +761,21 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", this.map.units, this.map.prefixes); } } - + /** * The units stored in this collection, without prefixes. * @@ -723,7 +783,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map<String, Unit> units; - + /** * The available prefixes for use. * @@ -731,95 +791,106 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map<String, UnitPrefix> prefixes; - + // caches private transient Collection<Unit> values = null; private transient Set<String> keySet = null; private transient Set<Entry<String, Unit>> entrySet = null; - + /** * Creates the {@code PrefixedUnitMap}. * - * @param units - * map mapping unit names to units - * @param prefixes - * map mapping prefix names to prefixes + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 * @since v0.2.0 */ - public PrefixedUnitMap(final Map<String, Unit> units, final Map<String, UnitPrefix> prefixes) { - // I am making unmodifiable maps to ensure I don't accidentally make changes. + public PrefixedUnitMap(final Map<String, Unit> units, + final Map<String, UnitPrefix> prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. this.units = Collections.unmodifiableMap(units); this.prefixes = Collections.unmodifiableMap(prefixes); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable map"); + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); } - + @Override public Unit compute(final String key, final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override - public Unit computeIfAbsent(final String key, final Function<? super String, ? extends Unit> mappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + public Unit computeIfAbsent(final String key, + final Function<? super String, ? extends Unit> mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public Unit computeIfPresent(final String key, final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public boolean containsKey(final Object key) { // First, test if there is a unit with the key if (this.units.containsKey(key)) return true; - + // Next, try to cast it to String if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to test for a unit using a non-string name."); + throw new IllegalArgumentException( + "Attempted to test for a unit using a non-string name."); final String unitName = (String) key; - + // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; int longestLength = 0; - + for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } - + return longestPrefix != null; } - + /** * {@inheritDoc} * * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method only tests for prefixless units. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method only tests for prefixless units. * </p> */ @Override public boolean containsValue(final Object value) { return this.units.containsValue(value); } - + @Override public Set<Entry<String, Unit>> entrySet() { if (this.entrySet == null) { @@ -827,56 +898,62 @@ public final class UnitDatabase { } return this.entrySet; } - + @Override public Unit get(final Object key) { // First, test if there is a unit with the key if (this.units.containsKey(key)) return this.units.get(key); - + // Next, try to cast it to String if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name."); + throw new IllegalArgumentException( + "Attempted to obtain a unit using a non-string name."); final String unitName = (String) key; - + // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; int longestLength = 0; - + for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } - + // if none found, returns null if (longestPrefix == null) return null; else { // get necessary data final String rest = unitName.substring(longestLength); - // this cast will not fail because I verified that it would work before selecting this prefix + // this cast will not fail because I verified that it would work + // before selecting this prefix final LinearUnit unit = (LinearUnit) this.get(rest); final UnitPrefix prefix = this.prefixes.get(longestPrefix); - + return unit.withPrefix(prefix); } } - + @Override public boolean isEmpty() { return this.units.isEmpty(); } - + @Override public Set<String> keySet() { if (this.keySet == null) { @@ -884,53 +961,64 @@ public final class UnitDatabase { } return this.keySet; } - + @Override public Unit merge(final String key, final Unit value, final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException("Cannot merge into an immutable map"); + throw new UnsupportedOperationException( + "Cannot merge into an immutable map"); } - + @Override public Unit put(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public void putAll(final Map<? extends String, ? extends Unit> m) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public Unit putIfAbsent(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public Unit remove(final Object key) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); } - + @Override public boolean remove(final Object key, final Object value) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); } - + @Override public Unit replace(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override - public boolean replace(final String key, final Unit oldValue, final Unit newValue) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public boolean replace(final String key, final Unit oldValue, + final Unit newValue) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override - public void replaceAll(final BiFunction<? super String, ? super Unit, ? extends Unit> function) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public void replaceAll( + final BiFunction<? super String, ? super Unit, ? extends Unit> function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override public int size() { if (this.units.isEmpty()) @@ -943,66 +1031,80 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite map of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", this.units, this.prefixes); } - + /** * {@inheritDoc} * * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method ignores prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method ignores prefixes. * </p> */ @Override public Collection<Unit> values() { if (this.values == null) { - this.values = Collections.unmodifiableCollection(this.units.values()); + this.values = Collections + .unmodifiableCollection(this.units.values()); } return this.values; } } - + /** * Replacements done to *all* expression types */ private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>(); - + // add data to expression replacements static { - // place brackets around any expression of the form "number unit", with or without the space + // add spaces around operators + for (final String operator : Arrays.asList("\\*", "/", "\\^")) { + EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), + " " + operator + " "); + } + + // replace multiple spaces with a single space + EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); + // place brackets around any expression of the form "number unit", with or + // without the space EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer - + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers after it + + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers + // after it + "\\s*" // optional space(s) + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters - + "(?!-?\\d)" // no number directly afterwards (avoids matching "1e3") + + "(?!-?\\d)" // no number directly afterwards (avoids matching + // "1e3") ), "\\($1 $2\\)"); } - + /** * A regular expression that separates names and expressions in unit files. */ - private static final Pattern NAME_EXPRESSION = Pattern.compile("(\\S+)\\s+(\\S.*)"); - + private static final Pattern NAME_EXPRESSION = Pattern + .compile("(\\S+)\\s+(\\S.*)"); + /** * The exponent operator * - * @param base - * base of exponentiation - * @param exponentUnit - * exponent + * @param base base of exponentiation + * @param exponentUnit exponent * @return result * @since 2019-04-10 * @since v0.2.0 */ - private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { + private static final LinearUnit exponentiateUnits(final LinearUnit base, + final LinearUnit exponentUnit) { // exponent function - first check if o2 is a number, if (exponentUnit.getBase().equals(SI.ONE.getBase())) { // then check if it is an integer, @@ -1012,12 +1114,39 @@ public final class UnitDatabase { return base.toExponent((int) (exponent + 0.5)); else // not an integer - throw new UnsupportedOperationException("Decimal exponents are currently not supported."); + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); } else // not a number throw new IllegalArgumentException("Exponents must be numbers."); } - + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2020-08-04 + */ + private static final LinearUnitValue exponentiateUnitValues( + final LinearUnitValue base, final LinearUnitValue exponentValue) { + // exponent function - first check if o2 is a number, + if (exponentValue.canConvertTo(SI.ONE)) { + // then check if it is an integer, + final double exponent = exponentValue.getValueExact(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + /** * The units in this system, excluding prefixes. * @@ -1025,7 +1154,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map<String, Unit> prefixlessUnits; - + /** * The unit prefixes in this system. * @@ -1033,7 +1162,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map<String, UnitPrefix> prefixes; - + /** * The dimensions in this system. * @@ -1041,7 +1170,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map<String, ObjectProduct<BaseDimension>> dimensions; - + /** * A map mapping strings to units (including prefixes) * @@ -1049,7 +1178,19 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map<String, Unit> units; - + + /** + * The rule that specifies when prefix repetition is allowed. It takes in one + * argument: a list of the prefixes being applied to the unit + * <p> + * The prefixes are inputted in <em>application order</em>. This means that + * testing whether "kilomegagigametre" is a valid unit is equivalent to + * running the following code (assuming all variables are defined correctly): + * <br> + * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} + */ + private Predicate<List<UnitPrefix>> prefixRepetitionRule; + /** * A parser that can parse unit expressions. * @@ -1059,21 +1200,41 @@ public final class UnitDatabase { private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>( this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*") + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); - + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) + .build(); + + /** + * A parser that can parse unit value expressions. + * + * @since 2020-08-04 + */ + private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnitValue) + .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .build(); + /** * A parser that can parse unit prefix expressions * * @since 2019-04-13 * @since v0.2.0 */ - private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); - + private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>( + this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + /** * A parser that can parse unit dimension expressions. * @@ -1081,9 +1242,10 @@ public final class UnitDatabase { * @since v0.2.0 */ private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); - + /** * Creates the {@code UnitsDatabase}. * @@ -1091,48 +1253,62 @@ public final class UnitDatabase { * @since v0.1.0 */ public UnitDatabase() { + this(prefixes -> true); + } + + /** + * Creates the {@code UnitsDatabase} + * + * @param prefixRepetitionRule the rule that determines when prefix + * repetition is allowed + * @since 2020-08-26 + */ + public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) { this.prefixlessUnits = new HashMap<>(); this.prefixes = new HashMap<>(); this.dimensions = new HashMap<>(); - this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); + this.prefixRepetitionRule = prefixRepetitionRule; + this.units = ConditionalExistenceCollections.conditionalExistenceMap( + new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), + entry -> this.prefixRepetitionRule + .test(this.getPrefixesFromName(entry.getKey()))); } - + /** * Adds a unit dimension to the database. * - * @param name - * dimension's name - * @param dimension - * dimension to add - * @throws NullPointerException - * if name or dimension is null + * @param name dimension's name + * @param dimension dimension to add + * @throws NullPointerException if name or dimension is null * @since 2019-03-14 * @since v0.2.0 */ - public void addDimension(final String name, final ObjectProduct<BaseDimension> dimension) { - this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), + public void addDimension(final String name, + final ObjectProduct<BaseDimension> dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(dimension, "dimension must not be null.")); } - + /** * Adds to the list from a line in a unit dimension file. * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages + * @param line line to look at + * @param lineCounter number of line, for error messages * @since 2019-04-10 * @since v0.2.0 */ - private void addDimensionFromLine(final String line, final long lineCounter) { + private void addDimensionFromLine(final String line, + final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), + lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1141,17 +1317,18 @@ public final class UnitDatabase { lineCounter)); final String name = lineMatcher.group(1); final String expression = lineMatcher.group(2); - + if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); + System.err.printf("Warning - line %d's dimension name ends in a space", + lineCounter); } - + // if expression is "!", search for an existing dimension // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsDimensionName(name)) - throw new IllegalArgumentException( - String.format("! used but no dimension found (line %d).", lineCounter)); + throw new IllegalArgumentException(String.format( + "! used but no dimension found (line %d).", lineCounter)); } else { // it's a unit, get the unit final ObjectProduct<BaseDimension> dimension; @@ -1161,20 +1338,17 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - + this.addDimension(name, dimension); } } - + /** * Adds a unit prefix to the database. * - * @param name - * prefix's name - * @param prefix - * prefix to add - * @throws NullPointerException - * if name or prefix is null + * @param name prefix's name + * @param prefix prefix to add + * @throws NullPointerException if name or prefix is null * @since 2019-01-14 * @since v0.1.0 */ @@ -1182,43 +1356,41 @@ public final class UnitDatabase { this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(prefix, "prefix must not be null.")); } - + /** * Adds a unit to the database. * - * @param name - * unit's name - * @param unit - * unit to add - * @throws NullPointerException - * if unit is null + * @param name unit's name + * @param unit unit to add + * @throws NullPointerException if unit is null * @since 2019-01-10 * @since v0.1.0 */ public void addUnit(final String name, final Unit unit) { - this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."), + this.prefixlessUnits.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(unit, "unit must not be null.")); } - + /** * Adds to the list from a line in a unit file. * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages + * @param line line to look at + * @param lineCounter number of line, for error messages * @since 2019-04-10 * @since v0.2.0 */ - private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { + private void addUnitOrPrefixFromLine(final String line, + final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), + lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1226,18 +1398,20 @@ public final class UnitDatabase { "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", lineCounter)); final String name = lineMatcher.group(1); - + final String expression = lineMatcher.group(2); - + if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); + System.err.printf("Warning - line %d's unit name ends in a space", + lineCounter); } - + // if expression is "!", search for an existing unit // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsUnitName(name)) - throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter)); + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); } else { if (name.endsWith("-")) { final UnitPrefix prefix; @@ -1257,17 +1431,16 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - + this.addUnit(name, unit); } } } - + /** * Tests if the database has a unit dimension with this name. * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-03-14 * @since v0.2.0 @@ -1275,12 +1448,11 @@ public final class UnitDatabase { public boolean containsDimensionName(final String name) { return this.dimensions.containsKey(name); } - + /** * Tests if the database has a unit prefix with this name. * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1288,12 +1460,12 @@ public final class UnitDatabase { public boolean containsPrefixName(final String name) { return this.prefixes.containsKey(name); } - + /** - * Tests if the database has a unit with this name, taking prefixes into consideration + * Tests if the database has a unit with this name, taking prefixes into + * consideration * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1301,7 +1473,7 @@ public final class UnitDatabase { public boolean containsUnitName(final String name) { return this.units.containsKey(name); } - + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 @@ -1310,7 +1482,50 @@ public final class UnitDatabase { public Map<String, ObjectProduct<BaseDimension>> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } - + + /** + * Evaluates a unit expression, following the same rules as + * {@link #getUnitFromExpression}. + * + * @param expression expression to parse + * @return {@code LinearUnitValue} representing value of expression + * @since 2020-08-04 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias, or a number with precision first + if (this.containsUnitName(expression)) + return this.getLinearUnitValue(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + // FIXME the previous operaton also breaks stuff like "1e-5" + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitValueExpressionParser.parseExpression(modifiedExpression); + } + /** * Gets a unit dimension from the database using its name. * @@ -1318,8 +1533,7 @@ public final class UnitDatabase { * This method accepts exponents, like "L^3" * </p> * - * @param name - * dimension's name + * @param name dimension's name * @return dimension * @since 2019-03-14 * @since v0.2.0 @@ -1328,102 +1542,125 @@ public final class UnitDatabase { Objects.requireNonNull(name, "name must not be null."); if (name.contains("^")) { final String[] baseAndExponent = name.split("\\^"); - - final ObjectProduct<BaseDimension> base = this.getDimension(baseAndExponent[0]); - + + final ObjectProduct<BaseDimension> base = this + .getDimension(baseAndExponent[0]); + final int exponent; try { - exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); + exponent = Integer + .parseInt(baseAndExponent[baseAndExponent.length - 1]); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Exponent must be an integer."); } - + return base.toExponent(exponent); } return this.dimensions.get(name); } - + /** * Uses the database's data to parse an expression into a unit dimension * <p> * The expression is a series of any of the following: * <ul> - * <li>The name of a unit dimension, which multiplies or divides the result based on preceding operators</li> - * <li>The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each - * other is equivalent to multiplication)</li> + * <li>The name of a unit dimension, which multiplies or divides the result + * based on preceding operators</li> + * <li>The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)</li> * <li>The operator '^' which exponentiates. Exponents must be integers.</li> * </ul> * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-04-13 * @since v0.2.0 */ - public ObjectProduct<BaseDimension> getDimensionFromExpression(final String expression) { + public ObjectProduct<BaseDimension> getDimensionFromExpression( + final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a dimension as an alias first if (this.containsDimensionName(expression)) return this.getDimension(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + return this.unitDimensionParser.parseExpression(modifiedExpression); } - + /** - * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an - * {@code IllegalArgumentException}. + * Gets a unit. If it is linear, cast it to a LinearUnit and return it. + * Otherwise, throw an {@code IllegalArgumentException}. * - * @param name - * unit's name + * @param name unit's name * @return unit * @since 2019-03-22 * @since v0.2.0 */ private LinearUnit getLinearUnit(final String name) { // see if I am using a function-unit like tempC(100) + Objects.requireNonNull(name, "name may not be null"); if (name.contains("(") && name.contains(")")) { // break it into function name and value final List<String> parts = Arrays.asList(name.split("\\(")); if (parts.size() != 2) - throw new IllegalArgumentException("Format nonlinear units like: unit(value)."); - + throw new IllegalArgumentException( + "Format nonlinear units like: unit(value)."); + // solve the function final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1)); + final double value = Double.parseDouble( + parts.get(1).substring(0, parts.get(1).length() - 1)); return LinearUnit.fromUnitValue(unit, value); } else { // get a linear unit final Unit unit = this.getUnit(name); + if (unit instanceof LinearUnit) return (LinearUnit) unit; else - throw new IllegalArgumentException(String.format("%s is not a linear unit.", name)); + throw new IllegalArgumentException( + String.format("%s is not a linear unit.", name)); } } - + + /** + * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be + * converted to their base units. + * + * @param name name of unit + * @return {@code LinearUnitValue} instance + * @since 2020-08-04 + */ + private LinearUnitValue getLinearUnitValue(final String name) { + try { + // try to parse it as a number - otherwise it is not a number! + final BigDecimal number = new BigDecimal(name); + + final double uncertainty = Math.pow(10, -number.scale()); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } + } + /** * Gets a unit prefix from the database from its name * - * @param name - * prefix's name + * @param name prefix's name * @return prefix * @since 2019-01-10 * @since v0.1.0 @@ -1435,53 +1672,87 @@ public final class UnitDatabase { return this.prefixes.get(name); } } - + + /** + * Gets all of the prefixes that are on a unit name, in application order. + * + * @param unitName name of unit + * @return prefixes + * @since 2020-08-26 + */ + List<UnitPrefix> getPrefixesFromName(final String unitName) { + final List<UnitPrefix> prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { + // find the longest prefix + String longestPrefixName = null; + int longestLength = name.length(); + + while (longestPrefixName == null) { + longestLength--; + if (longestLength <= 0) + throw new AssertionError( + "No prefix found in " + name + ", but it is not a unit!"); + if (this.prefixes.containsKey(name.substring(0, longestLength))) { + longestPrefixName = name.substring(0, longestLength); + } + } + + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); + } + return prefixes; + } + /** * Gets a unit prefix from a prefix expression * <p> - * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of - * another prefix + * Currently, prefix expressions are much simpler than unit expressions: They + * are either a number or the name of another prefix * </p> * - * @param expression - * expression to input + * @param expression expression to input * @return prefix - * @throws IllegalArgumentException - * if expression cannot be parsed - * @throws NullPointerException - * if any argument is null + * @throws IllegalArgumentException if expression cannot be parsed + * @throws NullPointerException if any argument is null * @since 2019-01-14 * @since v0.1.0 */ public UnitPrefix getPrefixFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias first if (this.containsUnitName(expression)) return this.getPrefix(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + return this.prefixExpressionParser.parseExpression(modifiedExpression); } - + + /** + * @return the prefixRepetitionRule + * @since 2020-08-26 + */ + public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { + return this.prefixRepetitionRule; + } + /** * Gets a unit from the database from its name, looking for prefixes. * - * @param name - * unit's name + * @param name unit's name * @return unit * @since 2019-01-10 * @since v0.1.0 @@ -1491,101 +1762,115 @@ public final class UnitDatabase { final double value = Double.parseDouble(name); return SI.ONE.times(value); } catch (final NumberFormatException e) { - return this.units.get(name); + final Unit unit = this.units.get(name); + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set<String> otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(unit.getPrimaryName().get()); + return unit.withName(NameSymbol.ofNullable(name, + unit.getSymbol().orElse(null), otherNames)); + } else if (!unit.getOtherNames().contains(name)) { + final Set<String> otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(name); + return unit.withName( + NameSymbol.ofNullable(unit.getPrimaryName().orElse(null), + unit.getSymbol().orElse(null), otherNames)); + } else + return unit; } - + } - + /** * Uses the database's unit data to parse an expression into a unit * <p> * The expression is a series of any of the following: * <ul> - * <li>The name of a unit, which multiplies or divides the result based on preceding operators</li> - * <li>The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each - * other is equivalent to multiplication)</li> + * <li>The name of a unit, which multiplies or divides the result based on + * preceding operators</li> + * <li>The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)</li> * <li>The operator '^' which exponentiates. Exponents must be integers.</li> * <li>A number which is multiplied or divided</li> * </ul> * This method only works with linear units. * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-01-07 * @since v0.1.0 */ public Unit getUnitFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias first if (this.containsUnitName(expression)) return this.getUnit(expression); - + // force operators to have spaces String modifiedExpression = expression; modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); modifiedExpression = modifiedExpression.replaceAll("-", " - "); - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + // the previous operation breaks negative numbers, fix them! // (i.e. -2 becomes - 2) for (int i = 0; i < modifiedExpression.length(); i++) { if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2)))) { + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); } } - + return this.unitExpressionParser.parseExpression(modifiedExpression); } - + /** - * Adds all dimensions from a file, using data from the database to parse them. + * Adds all dimensions from a file, using data from the database to parse + * them. * <p> - * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated - * by any number of tab characters. + * Each line in the file should consist of a name and an expression (parsed + * by getDimensionFromExpression) separated by any number of tab characters. * <p> * <p> * Allowed exceptions: * <ul> - * <li>Anything after a '#' character is considered a comment and ignored.</li> + * <li>Anything after a '#' character is considered a comment and + * ignored.</li> * <li>Blank lines are also ignored</li> - * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.</li> + * <li>If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.</li> * </ul> * - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null * @since 2019-01-13 * @since v0.1.0 */ - public void loadDimensionFile(final File file) { + public void loadDimensionFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try { long lineCounter = 0; - while (reader.ready()) { - this.addDimensionFromLine(reader.readLine(), ++lineCounter); + for (final String line : Files.readAllLines(file)) { + this.addDimensionFromLine(line, ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); @@ -1593,39 +1878,38 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * Adds all units from a file, using data from the database to parse them. * <p> - * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by - * any number of tab characters. + * Each line in the file should consist of a name and an expression (parsed + * by getUnitFromExpression) separated by any number of tab characters. * <p> * <p> * Allowed exceptions: * <ul> - * <li>Anything after a '#' character is considered a comment and ignored.</li> + * <li>Anything after a '#' character is considered a comment and + * ignored.</li> * <li>Blank lines are also ignored</li> - * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.</li> + * <li>If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.</li> * </ul> * - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null * @since 2019-01-13 * @since v0.1.0 */ - public void loadUnitsFile(final File file) { + public void loadUnitsFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try { long lineCounter = 0; - while (reader.ready()) { - this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); + for (final String line : Files.readAllLines(file)) { + this.addUnitOrPrefixFromLine(line, ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); @@ -1633,7 +1917,7 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * @return a map mapping prefix names to prefixes * @since 2019-04-13 @@ -1642,33 +1926,49 @@ public final class UnitDatabase { public Map<String, UnitPrefix> prefixMap() { return Collections.unmodifiableMap(this.prefixes); } - + /** - * @return a string stating the number of units, prefixes and dimensions in the database + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public final void setPrefixRepetitionRule( + Predicate<List<UnitPrefix>> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + } + + /** + * @return a string stating the number of units, prefixes and dimensions in + * the database */ @Override public String toString() { - return String.format("Unit Database with %d units, %d unit prefixes and %d dimensions", - this.prefixlessUnits.size(), this.prefixes.size(), this.dimensions.size()); + return String.format( + "Unit Database with %d units, %d unit prefixes and %d dimensions", + this.prefixlessUnits.size(), this.prefixes.size(), + this.dimensions.size()); } - + /** * Returns a map mapping unit names to units, including units with prefixes. * <p> - * The returned map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, - * some operations that only work with finite collections, like converting name/entry sets to arrays, will throw an - * {@code IllegalStateException}. + * The returned map is infinite in size if there is at least one unit and at + * least one prefix. If it is infinite, some operations that only work with + * finite collections, like converting name/entry sets to arrays, will throw + * an {@code IllegalStateException}. * </p> * <p> - * Specifically, the operations that will throw an IllegalStateException if the map is infinite in size are: + * Specifically, the operations that will throw an IllegalStateException if + * the map is infinite in size are: * <ul> * <li>{@code unitMap.entrySet().toArray()} (either overloading)</li> * <li>{@code unitMap.keySet().toArray()} (either overloading)</li> * </ul> * </p> * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's {@link PrefixedUnitMap#containsValue - * containsValue} and {@link PrefixedUnitMap#values() values()} methods currently ignore prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's + * {@link PrefixedUnitMap#containsValue containsValue} and + * {@link PrefixedUnitMap#values() values()} methods currently ignore + * prefixes. * </p> * * @return a map mapping unit names to units, including prefixed names @@ -1676,9 +1976,10 @@ public final class UnitDatabase { * @since v0.2.0 */ public Map<String, Unit> unitMap() { - return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map. + return this.units; // PrefixedUnitMap is immutable so I don't need to make + // an unmodifiable map. } - + /** * @return a map mapping unit names to units, ignoring prefixes * @since 2019-04-13 diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index 164172b..2b981b6 100644 --- a/src/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -18,17 +18,22 @@ package org.unitConverter.unit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; import org.junit.jupiter.api.Test; /** - * A test for the {@link UnitDatabase} class. This is NOT part of this program's public API. + * A test for the {@link UnitDatabase} class. This is NOT part of this program's + * public API. * * @author Adrien Hopkins * @since 2019-04-14 @@ -39,67 +44,52 @@ class UnitDatabaseTest { private static final Unit U = SI.METRE; private static final Unit V = SI.KILOGRAM; private static final Unit W = SI.SECOND; - + // used for testing expressions // J = U^2 * V / W^2 - private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)) + .dividedBy(SI.SECOND.toExponent(2)); private static final LinearUnit K = SI.KELVIN; - - private static final Unit NONLINEAR = Unit.fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); - + + private static final Unit NONLINEAR = Unit + .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); + // make the prefix values prime so I can tell which multiplications were made - private static final UnitPrefix A = UnitPrefix.valueOf(2); - private static final UnitPrefix B = UnitPrefix.valueOf(3); - private static final UnitPrefix C = UnitPrefix.valueOf(5); + private static final UnitPrefix A = UnitPrefix.valueOf(2) + .withName(NameSymbol.ofName("A")); + private static final UnitPrefix B = UnitPrefix.valueOf(3) + .withName(NameSymbol.ofName("B")); + private static final UnitPrefix C = UnitPrefix.valueOf(5) + .withName(NameSymbol.ofName("C")); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); - + /** - * Confirms that operations that shouldn't function for infinite databases throw an {@code IllegalStateException}. + * Confirms that operations that shouldn't function for infinite databases + * throw an {@code IllegalStateException}. * * @since 2019-05-03 */ @Test + // @Timeout(value = 5, unit = TimeUnit.SECONDS) public void testInfiniteSetExceptions() { // load units final UnitDatabase infiniteDatabase = new UnitDatabase(); - + infiniteDatabase.addUnit("J", J); infiniteDatabase.addUnit("K", K); - + infiniteDatabase.addPrefix("A", A); infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().entrySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().keySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } + + final Set<Entry<String, Unit>> entrySet = infiniteDatabase.unitMap() + .entrySet(); + final Set<String> keySet = infiniteDatabase.unitMap().keySet(); + assertThrows(IllegalStateException.class, () -> entrySet.toArray()); + assertThrows(IllegalStateException.class, () -> keySet.toArray()); } - + /** * Test that prefixes correctly apply to units. * @@ -109,23 +99,28 @@ class UnitDatabaseTest { @Test public void testPrefixes() { final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + + // test the getPrefixesFromName method + final List<UnitPrefix> expected = Arrays.asList(C, B, A); + assertEquals(expected, database.getPrefixesFromName("ABCU")); + // get the product final Unit abcuNonlinear = database.getUnit("ABCU"); assert abcuNonlinear instanceof LinearUnit; - + final LinearUnit abcu = (LinearUnit) abcuNonlinear; - assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); + assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), + abcu.getConversionFactor(), 1e-15); } - + /** * Tests the functionnalites of the prefixless unit map. * @@ -140,21 +135,22 @@ class UnitDatabaseTest { public void testPrefixlessUnitMap() { final UnitDatabase database = new UnitDatabase(); final Map<String, Unit> prefixlessUnits = database.unitMapPrefixless(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + // this should work because the map should be an auto-updating view assertTrue(prefixlessUnits.containsKey("U")); assertFalse(prefixlessUnits.containsKey("Z")); - + assertTrue(prefixlessUnits.containsValue(U)); assertFalse(prefixlessUnits.containsValue(NONLINEAR)); } - + /** - * Tests that the database correctly stores and retrieves units, ignoring prefixes. + * Tests that the database correctly stores and retrieves units, ignoring + * prefixes. * * @since 2019-04-14 * @since v0.2.0 @@ -162,18 +158,18 @@ class UnitDatabaseTest { @Test public void testPrefixlessUnits() { final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + assertTrue(database.containsUnitName("U")); assertFalse(database.containsUnitName("Z")); - + assertEquals(U, database.getUnit("U")); - assertEquals(null, database.getUnit("Z")); + assertThrows(NoSuchElementException.class, () -> database.getUnit("Z")); } - + /** * Test that unit expressions return the correct value. * @@ -184,30 +180,31 @@ class UnitDatabaseTest { public void testUnitExpressions() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); database.addUnit("fj", J.times(5)); database.addUnit("ej", J.times(8)); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + // first test - test prefixes and operations - final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C); + final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C) + .withPrefix(C); final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); - + assertEquals(expected1, actual1); - + // second test - test addition and subtraction final Unit expected2 = J.times(58); final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej"); - + assertEquals(expected2, actual2); } - + /** * Tests both the unit name iterator and the name-unit entry iterator * @@ -218,59 +215,64 @@ class UnitDatabaseTest { public void testUnitIterator() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("J", J); database.addUnit("K", K); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + final int NUM_UNITS = database.unitMapPrefixless().size(); final int NUM_PREFIXES = database.prefixMap().size(); - - final Iterator<String> nameIterator = database.unitMap().keySet().iterator(); - final Iterator<Entry<String, Unit>> entryIterator = database.unitMap().entrySet().iterator(); - + + final Iterator<String> nameIterator = database.unitMap().keySet() + .iterator(); + final Iterator<Entry<String, Unit>> entryIterator = database.unitMap() + .entrySet().iterator(); + int expectedLength = 1; int unitsWithThisLengthSoFar = 0; - + // loop 1000 times for (int i = 0; i < 1000; i++) { // expected length of next - if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { + if (unitsWithThisLengthSoFar >= NUM_UNITS + * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { expectedLength++; unitsWithThisLengthSoFar = 0; } - + // test that stuff is valid final String nextName = nameIterator.next(); final Unit nextUnit = database.getUnit(nextName); final Entry<String, Unit> nextEntry = entryIterator.next(); - + assertEquals(expectedLength, nextName.length()); assertEquals(nextName, nextEntry.getKey()); assertEquals(nextUnit, nextEntry.getValue()); - + unitsWithThisLengthSoFar++; } - + // test toString for consistency final String entryIteratorString = entryIterator.toString(); for (int i = 0; i < 3; i++) { assertEquals(entryIteratorString, entryIterator.toString()); } - + final String nameIteratorString = nameIterator.toString(); for (int i = 0; i < 3; i++) { assertEquals(nameIteratorString, nameIterator.toString()); } } - + /** - * Determine, given a unit name that could mean multiple things, which meaning is chosen. + * Determine, given a unit name that could mean multiple things, which + * meaning is chosen. * <p> - * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice. + * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this + * case, "AB-C-U" is the correct choice. * </p> * * @since 2019-04-14 @@ -280,28 +282,28 @@ class UnitDatabaseTest { public void testUnitPrefixCombinations() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("J", J); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); database.addPrefix("AB", AB); database.addPrefix("BC", BC); - + // test 1 - AB-C-J vs A-BC-J vs A-B-C-J final Unit expected1 = J.withPrefix(AB).withPrefix(C); final Unit actual1 = database.getUnit("ABCJ"); - + assertEquals(expected1, actual1); - + // test 2 - ABC-J vs AB-CJ vs AB-C-J database.addUnit("CJ", J.times(13)); database.addPrefix("ABC", UnitPrefix.valueOf(17)); - + final Unit expected2 = J.times(17); final Unit actual2 = database.getUnit("ABCJ"); - + assertEquals(expected2, actual2); } } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index c078cfc..c0711dc 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -16,7 +16,10 @@ */ package org.unitConverter.unit; +import static org.junit.Assert.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -25,7 +28,8 @@ import org.junit.jupiter.api.Test; import org.unitConverter.math.DecimalComparison; /** - * Testing the various Unit classes. This is NOT part of this program's public API. + * Testing the various Unit classes. This is NOT part of this program's public + * API. * * @author Adrien Hopkins * @since 2018-12-22 @@ -34,69 +38,109 @@ import org.unitConverter.math.DecimalComparison; class UnitTest { /** A random number generator */ private static final Random rng = ThreadLocalRandom.current(); - + @Test public void testAdditionAndSubtraction() { - final LinearUnit inch = SI.METRE.times(0.0254); - final LinearUnit foot = SI.METRE.times(0.3048); - + final LinearUnit inch = SI.METRE.times(0.0254) + .withName(NameSymbol.of("inch", "in")); + final LinearUnit foot = SI.METRE.times(0.3048) + .withName(NameSymbol.of("foot", "ft")); + assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); + + // test with LinearUnitValue + final LinearUnitValue value1 = LinearUnitValue.getExact(SI.METRE, 15); + final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120); + final LinearUnitValue value3 = LinearUnitValue.getExact(SI.METRE, 0.5); + final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); + + // make sure addition is done correctly + assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001); + assertEquals(15.5, value1.plus(value3).getValueExact()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValueExact(), + 0.001); + + // make sure addition uses the correct unit, and is still associative + // (ignoring floating-point rounding errors) + assertEquals(SI.METRE, value1.plus(value2).getUnit()); + assertEquals(SI.METRE, value1.plus(value2).plus(value3).getUnit()); + assertEquals(foot, value2.plus(value1).getUnit()); + assertTrue(value1.plus(value2).equals(value2.plus(value1), true)); + + // make sure errors happen when they should + assertThrows(IllegalArgumentException.class, () -> value1.plus(value4)); } - + @Test public void testConversion() { final LinearUnit metre = SI.METRE; final Unit inch = metre.times(0.0254); - + + final UnitValue value = UnitValue.of(inch, 75); + assertEquals(1.9, inch.convertTo(metre, 75), 0.01); - + assertEquals(1.9, value.convertTo(metre).getValue(), 0.01); + // try random stuff for (int i = 0; i < 1000; i++) { // initiate random values - final double conversionFactor = rng.nextDouble() * 1000000; - final double testValue = rng.nextDouble() * 1000000; + final double conversionFactor = UnitTest.rng.nextDouble() * 1000000; + final double testValue = UnitTest.rng.nextDouble() * 1000000; final double expected = testValue * conversionFactor; - + // test final Unit unit = SI.METRE.times(conversionFactor); final double actual = unit.convertToBase(testValue); - - assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); + + assertEquals(actual, expected, + expected * DecimalComparison.DOUBLE_EPSILON); } } - + @Test public void testEquals() { final LinearUnit metre = SI.METRE; final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); - + assertEquals(metre, meter); } - + + @Test + public void testIsMetric() { + final Unit metre = SI.METRE; + final Unit megasecond = SI.SECOND.withPrefix(SI.MEGA); + final Unit hour = SI.HOUR; + + assertTrue(metre.isMetric()); + assertTrue(megasecond.isMetric()); + assertFalse(hour.isMetric()); + } + @Test public void testMultiplicationAndDivision() { // test unit-times-unit multiplication - final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + final LinearUnit generatedJoule = SI.KILOGRAM + .times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); final LinearUnit actualJoule = SI.JOULE; - + assertEquals(generatedJoule, actualJoule); - + // test multiplication by conversion factors final LinearUnit kilometre = SI.METRE.times(1000); final LinearUnit hour = SI.SECOND.times(3600); final LinearUnit generatedKPH = kilometre.dividedBy(hour); - + final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); - + assertEquals(generatedKPH, actualKPH); } - + @Test public void testPrefixes() { final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); final LinearUnit actualKilometre = SI.METRE.times(1000); - + assertEquals(generatedKilometre, actualKilometre); } } diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java new file mode 100644 index 0000000..c138332 --- /dev/null +++ b/src/org/unitConverter/unit/UnitValue.java @@ -0,0 +1,172 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +/** + * A value expressed in a unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class UnitValue { + /** + * Creates a {@code UnitValue} from a unit and the associated value. + * + * @param unit unit to use + * @param value value to use + * @return {@code UnitValue} instance + */ + public static UnitValue of(Unit unit, double value) { + return new UnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value); + } + + private final Unit unit; + private final double value; + + /** + * @param unit the unit being used + * @param value the value being represented + */ + private UnitValue(Unit unit, Double value) { + this.unit = unit; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unit other) { + return this.unit.canConvertTo(other); + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final <W> boolean canConvertTo(Unitlike<W> other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a UnitlikeValue that represents the same value expressed in a + * different unitlike form. + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo( + U other) { + return UnitlikeValue.of(other, + this.unit.convertTo(other, this.getValue())); + } + + /** + * Returns a UnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnitValue)) + return false; + final UnitValue other = (UnitValue) obj; + return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) + && Double.doubleToLongBits( + this.getUnit().convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.getUnit().convertToBase(other.getValue())); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final Unit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.getUnit().getBase(), + this.getUnit().convertFromBase(this.getValue())); + } + + @Override + public String toString() { + final Optional<String> primaryName = this.getUnit().getPrimaryName(); + final Optional<String> symbol = this.getUnit().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnit().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} diff --git a/src/org/unitConverter/unit/Unitlike.java b/src/org/unitConverter/unit/Unitlike.java new file mode 100644 index 0000000..8077771 --- /dev/null +++ b/src/org/unitConverter/unit/Unitlike.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * An object that can convert a value between multiple forms (instances of the + * object); like a unit but the "converted value" can be any type. + * + * @since 2020-09-07 + */ +public abstract class Unitlike<V> implements Nameable { + /** + * Returns a unitlike form from its base and the functions it uses to convert + * to and from its base. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value expressed in the + * unitlike form's base and returns that value expressed + * in this unitlike form. + * @param converterTo function that accepts a value expressed in the + * unitlike form and returns that value expressed in the + * unit's base. + * @return a unitlike form that uses the provided functions to convert. + * @since 2020-09-07 + * @throws NullPointerException if any argument is null + */ + public static final <W> Unitlike<W> fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleFunction<W> converterFrom, + final ToDoubleFunction<W> converterTo) { + return new FunctionalUnitlike<>(base, NameSymbol.EMPTY, converterFrom, + converterTo); + } + + /** + * Returns a unitlike form from its base and the functions it uses to convert + * to and from its base. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value expressed in the + * unitlike form's base and returns that value expressed + * in this unitlike form. + * @param converterTo function that accepts a value expressed in the + * unitlike form and returns that value expressed in the + * unit's base. + * @param ns names and symbol of unit + * @return a unitlike form that uses the provided functions to convert. + * @since 2020-09-07 + * @throws NullPointerException if any argument is null + */ + public static final <W> Unitlike<W> fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleFunction<W> converterFrom, + final ToDoubleFunction<W> converterTo, final NameSymbol ns) { + return new FunctionalUnitlike<>(base, ns, converterFrom, converterTo); + } + + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct<BaseUnit> unitBase; + + /** + * This unit's name(s) and symbol + * + * @since 2020-09-07 + */ + private final NameSymbol nameSymbol; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct<BaseDimension> dimension = null; + + /** + * @param unitBase + * @since 2020-09-07 + */ + protected Unitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); + } + + /** + * Checks if a value expressed in this unitlike form can be converted to a + * value expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unit other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Checks if a value expressed in this unitlike form can be converted to a + * value expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final <W> boolean canConvertTo(final Unitlike<W> other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + protected abstract V convertFromBase(double value); + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-05-22 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(final Unit other, final V value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unitlike form to convert to + * @param value value to convert + * @param <W> type of value to convert to + * @return converted value + * @since 2020-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final <W> W convertTo(final Unitlike<W> other, final V value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + protected abstract double convertToBase(V value); + + /** + * @return combination of units that this unit is based on + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct<BaseUnit> getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct<BaseDimension> getDimension() { + if (this.dimension == null) { + final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); + final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); + + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } + + /** + * @return the nameSymbol + * @since 2020-09-07 + */ + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unitlike form") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); + } + + /** + * @param ns name(s) and symbol to use + * @return a copy of this unitlike form with provided name(s) and symbol + * @since 2020-09-07 + * @throws NullPointerException if ns is null + */ + public Unitlike<V> withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/org/unitConverter/unit/UnitlikeValue.java b/src/org/unitConverter/unit/UnitlikeValue.java new file mode 100644 index 0000000..79201c4 --- /dev/null +++ b/src/org/unitConverter/unit/UnitlikeValue.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Optional; + +/** + * + * @since 2020-09-07 + */ +final class UnitlikeValue<T extends Unitlike<V>, V> { + /** + * Gets a {@code UnitlikeValue<V>}. + * + * @since 2020-10-02 + */ + public static <T extends Unitlike<V>, V> UnitlikeValue<T, V> of(T unitlike, + V value) { + return new UnitlikeValue<>(unitlike, value); + } + + private final T unitlike; + private final V value; + + /** + * @param unitlike + * @param value + * @since 2020-09-07 + */ + private UnitlikeValue(T unitlike, V value) { + this.unitlike = unitlike; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unit other) { + return this.unitlike.canConvertTo(other); + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final <W> boolean canConvertTo(Unitlike<W> other) { + return this.unitlike.canConvertTo(other); + } + + /** + * Returns a UnitlikeValue that represents the same value expressed in a + * different unitlike form. + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo( + U other) { + return UnitlikeValue.of(other, + this.unitlike.convertTo(other, this.getValue())); + } + + /** + * Returns a UnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.unitlike.convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unitlike).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnitlike().convertTo(other, this.getValue())); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UnitlikeValue)) + return false; + final UnitlikeValue<?, ?> other = (UnitlikeValue<?, ?>) obj; + if (this.getUnitlike() == null) { + if (other.getUnitlike() != null) + return false; + } else if (!this.getUnitlike().equals(other.getUnitlike())) + return false; + if (this.getValue() == null) { + if (other.getValue() != null) + return false; + } else if (!this.getValue().equals(other.getValue())) + return false; + return true; + } + + /** + * @return the unitlike + * @since 2020-09-29 + */ + public final Unitlike<V> getUnitlike() { + return this.unitlike; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final V getValue() { + return this.value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.getUnitlike() == null ? 0 : this.getUnitlike().hashCode()); + result = prime * result + + (this.getValue() == null ? 0 : this.getValue().hashCode()); + return result; + } + + @Override + public String toString() { + final Optional<String> primaryName = this.getUnitlike().getPrimaryName(); + final Optional<String> symbol = this.getUnitlike().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnitlike() + .convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnitlike().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} |