diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2025-06-15 19:42:01 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2025-06-15 19:42:01 -0500 |
commit | 2fdbc084fd1d78f0b7633db34460b1195de264f3 (patch) | |
tree | 4c908950d9b049394f8160b8159b498aec586ecc /src/main/java/sevenUnits/unit | |
parent | ed53492243ecad8d975401a97f5b634328ad2c71 (diff) | |
parent | bccb5b5e3452421c81c1fb58f83391ba6584807c (diff) |
See the tag 'v1.0.0' or the changelog for more information about this
release.
Diffstat (limited to 'src/main/java/sevenUnits/unit')
19 files changed, 985 insertions, 1402 deletions
diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java index 3f1f75f..11b822e 100644 --- a/src/main/java/sevenUnits/unit/BaseDimension.java +++ b/src/main/java/sevenUnits/unit/BaseDimension.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019, 2022 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -23,26 +23,26 @@ import sevenUnits.utils.Nameable; /** * A dimension that defines a {@code BaseUnit} - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public final class BaseDimension implements Nameable { /** * Gets a {@code BaseDimension} with the provided name and symbol. - * + * * @param name name of dimension * @param symbol symbol used for dimension * @return dimension * @since 2019-10-16 + * @since v0.3.0 */ public static BaseDimension valueOf(final String name, final String symbol) { return new BaseDimension(name, symbol); } - /** - * The name of the dimension. - */ + /** The name of the dimension. */ private final String name; /** * The symbol used by the dimension. Symbols should be short, generally one @@ -52,20 +52,19 @@ public final class BaseDimension implements Nameable { /** * Creates the {@code BaseDimension}. - * + * * @param name name of unit * @param symbol symbol of unit * @throws NullPointerException if any argument is null * @since 2019-10-16 + * @since v0.3.0 */ private BaseDimension(final String name, final String symbol) { this.name = Objects.requireNonNull(name, "name must not be null."); this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } - /** - * @since v0.4.0 - */ + /** @since v0.4.0 */ @Override public NameSymbol getNameSymbol() { return NameSymbol.of(this.name, this.symbol); diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java index fe85a7b..13e76d9 100644 --- a/src/main/java/sevenUnits/unit/BaseUnit.java +++ b/src/main/java/sevenUnits/unit/BaseUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -28,19 +28,21 @@ import sevenUnits.utils.NameSymbol; * 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 + * @since v0.3.0 */ 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 * @return base unit * @since 2019-10-16 + * @since v0.3.0 */ public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { @@ -49,31 +51,32 @@ 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 + * @param otherNames other possible names of unit * @return base unit * @since 2019-10-21 + * @since v0.3.0 */ 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. - */ + /** 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 * @since 2019-10-16 + * @since v0.3.0 */ private BaseUnit(final BaseDimension dimension, final String primaryName, final String symbol, final Set<String> otherNames) { @@ -86,9 +89,10 @@ public final class BaseUnit extends Unit { * 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 + * @since v0.3.0 */ public LinearUnit asLinearUnit() { return LinearUnit.valueOf(this.getBase(), 1); @@ -107,8 +111,9 @@ public final class BaseUnit extends Unit { /** * @return dimension * @since 2019-10-16 + * @since v0.3.0 */ - public final BaseDimension getBaseDimension() { + public BaseDimension getBaseDimension() { return this.dimension; } diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java index 69a3c05..408e9e8 100644 --- a/src/main/java/sevenUnits/unit/BritishImperial.java +++ b/src/main/java/sevenUnits/unit/BritishImperial.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -20,16 +20,21 @@ import sevenUnits.utils.NameSymbol; /** * A static utility class that contains units in the British Imperial system. - * + * * @author Adrien Hopkins * @since 2019-10-21 + * @since v0.3.0 */ +// this class is just constants, most of which are obvious from the variable name +// so no need to check for missing values +@SuppressWarnings("javadoc") public final class BritishImperial { /** * Imperial units that measure area - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Area { public static final LinearUnit SQUARE_FOOT = Length.FOOT.toExponent(2); @@ -42,9 +47,10 @@ public final class BritishImperial { /** * Imperial units that measure length - * + * * @author Adrien Hopkins * @since 2019-10-28 + * @since v0.3.0 */ public static final class Length { /** @@ -55,11 +61,18 @@ public final class BritishImperial { public static final LinearUnit FOOT = YARD.dividedBy(3); public static final LinearUnit INCH = FOOT.dividedBy(12); public static final LinearUnit THOU = INCH.dividedBy(1000); + /** A chain, equal to 22 yards. */ public static final LinearUnit CHAIN = YARD.times(22); + /** A furlong, equal to 10 chains or 220 yards. */ public static final LinearUnit FURLONG = CHAIN.times(10); + /** A mile, equal to 8 furlongs or 1760 yards. */ public static final LinearUnit MILE = FURLONG.times(8); + /** A league, equal to 3 miles. */ public static final LinearUnit LEAGUE = MILE.times(3); + /** + * A nautical mile, around 1 arcminute around the Earth's circumference. + */ public static final LinearUnit NAUTICAL_MILE = Metric.METRE.times(1852); public static final LinearUnit CABLE = NAUTICAL_MILE.dividedBy(10); public static final LinearUnit FATHOM = CABLE.dividedBy(100); @@ -70,9 +83,10 @@ public final class BritishImperial { /** * British Imperial units that measure mass. - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Mass { public static final LinearUnit POUND = Metric.GRAM.times(453.59237); @@ -88,9 +102,10 @@ public final class BritishImperial { /** * British Imperial units that measure volume - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Volume { public static final LinearUnit FLUID_OUNCE = Metric.LITRE diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java index 6de446f..1d55b42 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnit.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -24,30 +24,33 @@ import sevenUnits.utils.ObjectProduct; /** * A unit that uses functional objects to convert to and from its base. - * + * * @author Adrien Hopkins * @since 2019-05-22 + * @since v0.3.0 */ final class FunctionalUnit extends Unit { /** * A function that accepts a value expressed in the unit's base and returns * that value expressed in this unit. - * + * * @since 2019-05-22 + * @since v0.3.0 */ private final DoubleUnaryOperator converterFrom; /** * A function that accepts a value expressed in the unit and returns that * value expressed in the unit's base. - * + * * @since 2019-05-22 + * @since v0.3.0 */ private final DoubleUnaryOperator converterTo; /** * Creates the {@code FunctionalUnit}. - * + * * @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. @@ -55,6 +58,7 @@ final class FunctionalUnit extends Unit { * and returns that value expressed in the unit's base. * @throws NullPointerException if any argument is null * @since 2019-05-22 + * @since v0.3.0 */ public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, @@ -68,14 +72,16 @@ final class FunctionalUnit extends Unit { /** * Creates the {@code FunctionalUnit}. - * + * * @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 name and symbol of resulting unit * @throws NullPointerException if any argument is null * @since 2019-05-22 + * @since v0.3.0 */ public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, @@ -89,7 +95,7 @@ final class FunctionalUnit extends Unit { /** * {@inheritDoc} - * + * * Uses {@code converterFrom} to convert. */ @Override @@ -99,7 +105,7 @@ final class FunctionalUnit extends Unit { /** * {@inheritDoc} - * + * * Uses {@code converterTo} to convert. */ @Override diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java deleted file mode 100644 index e9b4d1f..0000000 --- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * 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 sevenUnits.unit; - -import java.util.function.DoubleFunction; -import java.util.function.ToDoubleFunction; - -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.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/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 6489229..85f6dd9 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -26,19 +26,21 @@ import sevenUnits.utils.UncertainDouble; /** * 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 + * @since v0.3.0 */ 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' - * + * * @param unit unit to convert * @param value value to convert * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if unit is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value) { @@ -50,12 +52,13 @@ 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' - * + * * @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 + * @since v0.3.0 * @throws NullPointerException if unit or ns is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value, @@ -66,32 +69,26 @@ public final class LinearUnit extends Unit { } /** + * @param unit unit to get base version of * @return the base unit associated with {@code unit}, as a * {@code LinearUnit}. * @since 2020-10-02 + * @since v0.3.0 */ 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 * @return product of base and conversion factor * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if unitBase is null */ public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, @@ -103,12 +100,13 @@ public final class LinearUnit extends Unit { * 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 * @return product of base and conversion factor * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if unitBase is null */ public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, @@ -118,21 +116,23 @@ public final class LinearUnit extends Unit { /** * The value of this unit as represented in its base form. Mathematically, - * + * * <pre> * this = conversionFactor * getBase() * </pre> - * + * * @since 2019-10-16 + * @since v0.3.0 */ private final double conversionFactor; /** * Creates the {@code LinearUnit}. - * + * * @param unitBase base of linear unit * @param conversionFactor conversion factor between base and unit * @since 2019-10-16 + * @since v0.3.0 */ private LinearUnit(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor, final NameSymbol ns) { @@ -142,7 +142,7 @@ public final class LinearUnit extends Unit { /** * {@inheritDoc} - * + * * Converts by dividing by {@code conversionFactor} */ @Override @@ -153,11 +153,12 @@ public final class LinearUnit extends Unit { /** * 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 + * @since v0.3.0 * @throws IllegalArgumentException if {@code other} is incompatible for * conversion with this unit (as tested by * {@link Unit#canConvertTo}). @@ -169,15 +170,14 @@ public final class LinearUnit extends Unit { 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)); + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); } /** * {@inheritDoc} - * + * * Converts by multiplying by {@code conversionFactor} */ @Override @@ -187,8 +187,9 @@ public final class LinearUnit extends Unit { /** * Converts an {@code UncertainDouble} to the base unit. - * + * * @since 2020-09-07 + * @since v0.3.0 */ UncertainDouble convertToBase(final UncertainDouble value) { return value.timesExact(this.getConversionFactor()); @@ -196,7 +197,7 @@ public final class LinearUnit extends Unit { /** * Divides this unit by a scalar. - * + * * @param divisor scalar to divide by * @return quotient * @since 2018-12-23 @@ -208,7 +209,7 @@ public final class LinearUnit extends Unit { /** * Returns the quotient of this unit and another. - * + * * @param divisor unit to divide by * @return quotient of two units * @throws NullPointerException if {@code divisor} is null @@ -219,22 +220,35 @@ public final class LinearUnit extends Unit { Objects.requireNonNull(divisor, "other must not be null"); // divide the units - final ObjectProduct<BaseUnit> base = this.getBase() - .dividedBy(divisor.getBase()); + final var base = this.getBase().dividedBy(divisor.getBase()); return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); } /** * {@inheritDoc} - * + * * Uses the base and conversion factor of units to test for equality. */ @Override public boolean equals(final Object obj) { if (!(obj instanceof LinearUnit)) return false; - final LinearUnit other = (LinearUnit) obj; + final var other = (LinearUnit) obj; + return Objects.equals(this.getBase(), other.getBase()) + && Double.compare(this.getConversionFactor(), + other.getConversionFactor()) == 0; + } + + /** + * @param other unit to test equality with + * @return true iff this unit and other are equal, ignoring small differences + * caused by floating-point error. + * + * @apiNote This method is not transitive, so it cannot be used as an equals + * method. + */ + public boolean equalsApproximately(final LinearUnit other) { return Objects.equals(this.getBase(), other.getBase()) && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); @@ -243,6 +257,7 @@ public final class LinearUnit extends Unit { /** * @return conversion factor * @since 2019-10-16 + * @since v0.3.0 */ public double getConversionFactor() { return this.conversionFactor; @@ -250,13 +265,13 @@ public final class LinearUnit extends Unit { /** * {@inheritDoc} - * + * * Uses the base and conversion factor to compute a hash code. */ @Override public int hashCode() { return 31 * this.getBase().hashCode() - + DecimalComparison.hash(this.getConversionFactor()); + + Double.hashCode(this.getConversionFactor()); } /** @@ -264,6 +279,7 @@ public final class LinearUnit extends Unit { * is a {@code BaseUnit b} where * {@code b.asLinearUnit().equals(this)} returns {@code true}.) * @since 2019-10-16 + * @since v0.3.0 */ public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); @@ -272,6 +288,7 @@ public final class LinearUnit extends Unit { /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 + * @since v0.3.0 */ public boolean isCoherent() { return this.getConversionFactor() == 1; @@ -285,7 +302,7 @@ public final class LinearUnit extends Unit { * does not meet this condition, an {@code IllegalArgumentException} will be * thrown. * </p> - * + * * @param subtrahend unit to subtract * @return difference of units * @throws IllegalArgumentException if {@code subtrahend} is not compatible @@ -316,7 +333,7 @@ public final class LinearUnit extends Unit { * does not meet this condition, an {@code IllegalArgumentException} will be * thrown. * </p> - * + * * @param addend unit to add * @return sum of units * @throws IllegalArgumentException if {@code addend} is not compatible for @@ -341,7 +358,7 @@ public final class LinearUnit extends Unit { /** * Multiplies this unit by a scalar. - * + * * @param multiplier scalar to multiply by * @return product * @since 2018-12-23 @@ -353,7 +370,7 @@ public final class LinearUnit extends Unit { /** * Returns the product of this unit and another. - * + * * @param multiplier unit to multiply by * @return product of two units * @throws NullPointerException if {@code multiplier} is null @@ -364,8 +381,7 @@ public final class LinearUnit extends Unit { Objects.requireNonNull(multiplier, "other must not be null"); // multiply the units - final ObjectProduct<BaseUnit> base = this.getBase() - .times(multiplier.getBase()); + final var base = this.getBase().times(multiplier.getBase()); return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); } @@ -379,7 +395,7 @@ public final class LinearUnit extends Unit { /** * Returns this unit but to an exponent. - * + * * @param exponent exponent to exponentiate unit to * @return exponentiated unit * @since 2019-01-15 @@ -390,6 +406,21 @@ public final class LinearUnit extends Unit { Math.pow(this.conversionFactor, exponent)); } + /** + * Returns this unit to an exponent, rounding the resulting dimensions to the + * nearest integer. + * + * @param exponent exponent to raise unit to + * @return result of rounded exponentation + * @since 2024-08-22 + * @since v1.0.0 + * @see ObjectProduct#toExponentRounded + */ + public LinearUnit toExponentRounded(final double exponent) { + return valueOf(this.getBase().toExponentRounded(exponent), + Math.pow(this.conversionFactor, exponent)); + } + @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); @@ -404,7 +435,7 @@ public final class LinearUnit extends Unit { * have a symbol. <br> * This method ignores alternate names of both this unit and the provided * prefix. - * + * * @param prefix prefix to apply * @return unit with prefix * @since 2019-03-18 @@ -412,7 +443,7 @@ public final class LinearUnit extends Unit { * @throws NullPointerException if prefix is null */ public LinearUnit withPrefix(final UnitPrefix prefix) { - final LinearUnit unit = this.times(prefix.getMultiplier()); + final var unit = this.times(prefix.getMultiplier()); // create new name and symbol, if possible final String name; diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index 3a9428b..ce60e3b 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -17,33 +17,38 @@ package sevenUnits.unit; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -import java.util.Optional; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.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 + * @since v0.3.0 */ public final class LinearUnitValue { + /** The value 1 as a LinearUnitValue. */ public static final LinearUnitValue ONE = getExact(Metric.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 + * @since v0.3.0 */ - public static final LinearUnitValue getExact(final LinearUnit unit, + public static LinearUnitValue getExact(final LinearUnit unit, final double value) { return new LinearUnitValue( Objects.requireNonNull(unit, "unit must not be null"), @@ -52,14 +57,14 @@ public final class LinearUnitValue { /** * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param uncertainty absolute uncertainty of value + * + * @param unit unit to express with + * @param value value to express * @return uncertain {@code LinearUnitValue} instance * @since 2020-07-26 + * @since v0.3.0 */ - public static final LinearUnitValue of(final LinearUnit unit, + public static LinearUnitValue of(final LinearUnit unit, final UncertainDouble value) { return new LinearUnitValue( Objects.requireNonNull(unit, "unit must not be null"), @@ -74,6 +79,7 @@ public final class LinearUnitValue { * @param unit unit to express as * @param value value to express * @since 2020-07-26 + * @since v0.3.0 */ private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { this.unit = unit; @@ -84,8 +90,9 @@ public final class LinearUnitValue { * @return this value as a {@code UnitValue}. All uncertainty information is * removed from the returned value. * @since 2020-08-04 + * @since v0.3.0 */ - public final UnitValue asUnitValue() { + public UnitValue asUnitValue() { return UnitValue.of(this.unit, this.value.value()); } @@ -93,29 +100,68 @@ public final class LinearUnitValue { * @param other a {@code LinearUnit} * @return true iff this value can be represented with {@code other}. * @since 2020-07-26 + * @since v0.3.0 */ - public final boolean canConvertTo(final LinearUnit other) { + public 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 + * @since v0.3.0 */ - public final LinearUnitValue convertTo(final LinearUnit other) { + public LinearUnitValue convertTo(final LinearUnit other) { return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); } /** + * Convert a LinearUnitValue to a sum of multiple units, where all but the + * last have exact integer values. + * + * @param others units to convert to + * @return terms of the sum + * @throws IllegalArgumentException if no units are provided or units + * provided have incompatible bases + * @since 2024-08-15 + * @since v1.0.0 + */ + public List<LinearUnitValue> convertToMultiple( + final List<LinearUnit> others) { + if (others.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + for (final LinearUnit unit : others) { + if (!Objects.equals(this.unit.getBase(), unit.getBase())) + throw new IllegalArgumentException( + "All provided units must have the same base as the value."); + } + + var remaining = this; + final List<LinearUnitValue> values = new ArrayList<>(others.size()); + for (final LinearUnit unit : others.subList(0, others.size() - 1)) { + final var remainingInUnit = remaining.convertTo(unit); + final var value = getExact(unit, + Math.floor(remainingInUnit.getValueExact() + 1e-12)); + values.add(value); + remaining = remaining.minus(value); + } + + final var lastValue = remaining.convertTo(others.get(others.size() - 1)); + values.add(lastValue); + return values; + } + + /** * Divides this value by a scalar - * + * * @param divisor value to divide by * @return multiplied value * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue dividedBy(final double divisor) { return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); @@ -123,10 +169,11 @@ public final class LinearUnitValue { /** * Divides this value by another value - * + * * @param divisor value to multiply by * @return quotient * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue dividedBy(final LinearUnitValue divisor) { return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), @@ -137,15 +184,16 @@ public final class LinearUnitValue { * 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 + * @since v0.3.0 * @see #equals(Object, boolean) */ @Override public boolean equals(final Object obj) { if (!(obj instanceof LinearUnitValue)) return false; - final LinearUnitValue other = (LinearUnitValue) obj; + final var other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && this.unit.convertToBase(this.value) .equals(other.unit.convertToBase(other.value)); @@ -155,18 +203,22 @@ public final class LinearUnitValue { * 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. - * + * + * @param obj object to test equality with + * @param avoidFPErrors if true, this method will attempt to avoid + * floating-point errors, at the cost of not always + * being transitive. + * @return true iff this and obj are equal + * * @since 2020-07-28 + * @since v0.3.0 */ 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; + final var other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && DecimalComparison.equals(this.unit.convertToBase(this.value), other.unit.convertToBase(other.value)); @@ -175,16 +227,17 @@ public final class LinearUnitValue { /** * @param other another {@code LinearUnitValue} * @return true iff this and other are within each other's uncertainty range - * + * * @since 2020-07-26 + * @since v0.3.0 */ 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); + final var base = LinearUnit.valueOf(this.unit.getBase(), 1); + final var thisBase = this.convertTo(base); + final var otherBase = other.convertTo(base); return thisBase.value.equivalent(otherBase.value); } @@ -192,24 +245,27 @@ public final class LinearUnitValue { /** * @return the unit * @since 2020-09-29 + * @since v0.3.0 */ - public final LinearUnit getUnit() { + public LinearUnit getUnit() { return this.unit; } /** * @return the value * @since 2020-09-29 + * @since v0.3.0 */ - public final UncertainDouble getValue() { + public UncertainDouble getValue() { return this.value; } /** * @return the exact value * @since 2020-09-07 + * @since v0.3.0 */ - public final double getValueExact() { + public double getValueExact() { return this.value.value(); } @@ -222,12 +278,13 @@ public final class LinearUnitValue { /** * 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 + * @since v0.3.0 */ public LinearUnitValue minus(final LinearUnitValue subtrahend) { Objects.requireNonNull(subtrahend, "subtrahend may not be null"); @@ -237,19 +294,20 @@ public final class LinearUnitValue { "Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + final var 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 + * @since v0.3.0 */ public LinearUnitValue plus(final LinearUnitValue addend) { Objects.requireNonNull(addend, "addend may not be null"); @@ -259,17 +317,18 @@ public final class LinearUnitValue { "Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - final LinearUnitValue otherConverted = addend.convertTo(this.unit); + final var 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 + * @since v0.3.0 */ public LinearUnitValue times(final double multiplier) { return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); @@ -277,10 +336,11 @@ public final class LinearUnitValue { /** * Multiplies this value by another value - * + * * @param multiplier value to multiply by * @return product * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue times(final LinearUnitValue multiplier) { return LinearUnitValue.of(this.unit.times(multiplier.unit), @@ -289,16 +349,32 @@ public final class LinearUnitValue { /** * Raises a value to an exponent - * + * * @param exponent exponent to raise to * @return result of exponentiation * @since 2020-07-28 + * @since v0.3.0 */ public LinearUnitValue toExponent(final int exponent) { return LinearUnitValue.of(this.unit.toExponent(exponent), this.value.toExponentExact(exponent)); } + /** + * Raises this value to an exponent, rounding all dimensions to integers. + * + * @param exponent exponent to raise this value to + * @return result of exponentation + * + * @since 2024-08-22 + * @since v1.0.0 + * @see ObjectProduct#toExponentRounded + */ + public LinearUnitValue toExponentRounded(final double exponent) { + return LinearUnitValue.of(this.unit.toExponentRounded(exponent), + this.value.toExponentExact(exponent)); + } + @Override public String toString() { return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN); @@ -313,23 +389,28 @@ public final class LinearUnitValue { * single numbers. * <p> * Non-exact values are rounded intelligently based on their uncertainty. - * + * + * @param showUncertainty whether to show the value's uncertainty + * @param roundingMode how to round numbers in this string + * @return string representing this value + * * @since 2020-07-26 + * @since v0.3.0 */ public String toString(final boolean showUncertainty, RoundingMode roundingMode) { - final Optional<String> primaryName = this.unit.getPrimaryName(); - final Optional<String> symbol = this.unit.getSymbol(); - final String chosenName = symbol.orElse(primaryName.orElse(null)); + final var primaryName = this.unit.getPrimaryName(); + final var symbol = this.unit.getSymbol(); + final var chosenName = symbol.orElse(primaryName.orElse(null)); - final UncertainDouble baseValue = this.unit.convertToBase(this.value); + final var baseValue = this.unit.convertToBase(this.value); // get rounded strings // if showUncertainty is true, add brackets around the string - final String valueString = (showUncertainty ? "(" : "") + final var valueString = (showUncertainty ? "(" : "") + this.value.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); - final String baseValueString = (showUncertainty ? "(" : "") + final var baseValueString = (showUncertainty ? "(" : "") + baseValue.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); @@ -338,7 +419,6 @@ public final class LinearUnitValue { return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase() .toString(unit -> unit.getSymbol().orElseThrow())); - else - return String.format("%s %s", valueString, chosenName); + return String.format("%s %s", valueString, chosenName); } } diff --git a/src/main/java/sevenUnits/unit/LoadingException.java b/src/main/java/sevenUnits/unit/LoadingException.java new file mode 100644 index 0000000..2a75c99 --- /dev/null +++ b/src/main/java/sevenUnits/unit/LoadingException.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2024, 2025 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 sevenUnits.unit; + +import java.nio.file.Path; +import java.util.Optional; + +/** + * An exception that occurred when loading a file. This wrapper class adds more + * info about the error. + * + * @author Adrien Hopkins + * @since 2024-08-22 + * @since v1.0.0 + */ +public final class LoadingException extends RuntimeException { + /** The type of file that was being loaded. */ + public enum FileType { + @SuppressWarnings("javadoc") + UNIT, @SuppressWarnings("javadoc") + DIMENSION + } + + private static final long serialVersionUID = -8167971828216907607L; + + private final long lineNumber; + private final String line; + private final Optional<Path> file; + + private final FileType fileType; + + private final RuntimeException problem; + + /** + * Create a LoadingException from some information, without a file. + * + * @param lineNumber line number error happened on + * @param line text of invalid line + * @param fileType type of file + * @param problem problem, as an Exception + */ + public LoadingException(long lineNumber, String line, FileType fileType, + RuntimeException problem) { + super(problem); + this.lineNumber = lineNumber; + this.line = line; + this.file = Optional.empty(); + this.fileType = fileType; + this.problem = problem; + } + + /** + * Create a LoadingException from some information, with a file. + * + * @param lineNumber line number error happened on + * @param line text of invalid line + * @param file file error happened on + * @param fileType type of file + * @param problem problem, as an Exception + */ + public LoadingException(long lineNumber, String line, Path file, + FileType fileType, RuntimeException problem) { + super(problem); + this.lineNumber = lineNumber; + this.line = line; + this.file = Optional.of(file); + this.fileType = fileType; + this.problem = problem; + } + + /** @return the file this error happened in, if there is one */ + public Optional<Path> file() { + return this.file; + } + + /** @return type of file that this error happened in */ + public FileType fileType() { + return this.fileType; + } + + @Override + public String getMessage() { + return this.file + .map(f -> String.format( + "Error parsing line %d of %s file '%s' (\"%s\"): %s", + this.lineNumber, this.fileType.toString().toLowerCase(), f, + this.line, this.problem)) + .orElse(String.format( + "Error parsing line %d of %s stream (\"%s\"): %s", + this.lineNumber, this.fileType.toString().toLowerCase(), + this.line, this.problem)); + } + + /** @return text of line that caused this error */ + public String line() { + return this.line; + } + + /** @return number of line that caused this error */ + public long lineNumber() { + return this.lineNumber; + } + + /** @return the error, as an exception */ + public RuntimeException problem() { + return this.problem; + } +} diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 7841987..e712dc3 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018, 2021, 2022, 2024, 2025 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 @@ -24,19 +24,23 @@ import sevenUnits.utils.ObjectProduct; /** * 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}: - * + * * <pre> * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO); * </pre> - * - * + * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ +// this class is just constants, most of which are obvious from the variable name +// so no need to check for missing values +@SuppressWarnings("javadoc") public final class Metric { /// dimensions used by SI units // base dimensions, as BaseDimensions @@ -67,8 +71,10 @@ public final class Metric { } /// 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) + // and because these objects are only used outside this class, + // where hiding is not a problem. @SuppressWarnings("hiding") public static final class BaseUnits { public static final BaseUnit METRE = BaseUnit @@ -101,9 +107,10 @@ public final class Metric { /** * Constants that relate to the SI or other systems. - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Constants { public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND) @@ -343,7 +350,7 @@ public final class Metric { 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)) + Math::log10, dB -> Math.pow(10, dB)) .withName(NameSymbol.of("bel", "B")); public static final Unit DECIBEL = Unit .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java deleted file mode 100644 index 950c547..0000000 --- a/src/main/java/sevenUnits/unit/MultiUnit.java +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 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 sevenUnits.unit; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.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/main/java/sevenUnits/unit/USCustomary.java b/src/main/java/sevenUnits/unit/USCustomary.java index fce829e..ef12043 100644 --- a/src/main/java/sevenUnits/unit/USCustomary.java +++ b/src/main/java/sevenUnits/unit/USCustomary.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2024, 2025 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 @@ -18,16 +18,21 @@ package sevenUnits.unit; /** * A static utility class that contains units in the US Customary system. - * + * * @author Adrien Hopkins * @since 2019-10-21 + * @since v0.3.0 */ +// this class is just constants, most of which are obvious from the variable name +// so no need to check for missing values +@SuppressWarnings("javadoc") public final class USCustomary { /** * US Customary units that measure area - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Area { public static final LinearUnit SQUARE_SURVEY_FOOT = Length.SURVEY_FOOT @@ -43,9 +48,10 @@ public final class USCustomary { /** * US Customary units that measure length - * + * * @author Adrien Hopkins * @since 2019-10-28 + * @since v0.3.0 */ public static final class Length { public static final LinearUnit FOOT = BritishImperial.Length.FOOT; @@ -73,9 +79,10 @@ public final class USCustomary { /** * mass units - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Mass { public static final LinearUnit GRAIN = BritishImperial.Mass.GRAIN; @@ -93,9 +100,10 @@ public final class USCustomary { /** * Volume units - * + * * @author Adrien Hopkins * @since 2019-11-08 + * @since v0.3.0 */ public static final class Volume { public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 59e928a..40e6e0d 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -28,22 +28,23 @@ import sevenUnits.utils.ObjectProduct; /** * A unit that is composed of base units. - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public abstract class Unit implements Nameable { /** * 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: - * + * * {@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. @@ -51,6 +52,7 @@ public abstract class Unit implements Nameable { * and returns that value expressed in the unit's base. * @return a unit that uses the provided functions to convert. * @since 2019-05-22 + * @since v0.3.0 * @throws NullPointerException if any argument is null */ public static final Unit fromConversionFunctions( @@ -63,14 +65,14 @@ public abstract class Unit implements Nameable { /** * 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: - * + * * {@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. @@ -79,6 +81,7 @@ public abstract class Unit implements Nameable { * @param ns names and symbol of unit * @return a unit that uses the provided functions to convert. * @since 2019-05-22 + * @since v0.3.0 * @throws NullPointerException if any argument is null */ public static final Unit fromConversionFunctions( @@ -90,15 +93,17 @@ public abstract class Unit implements Nameable { /** * The combination of units that this unit is based on. - * + * * @since 2019-10-16 + * @since v0.3.0 */ private final ObjectProduct<BaseUnit> unitBase; /** * This unit's name(s) and symbol - * + * * @since 2020-09-07 + * @since v0.3.0 */ private final NameSymbol nameSymbol; @@ -106,28 +111,30 @@ public abstract class Unit implements Nameable { * Cache storing the result of getDimension() * * @since 2019-10-16 + * @since v0.3.0 */ private transient ObjectProduct<BaseDimension> dimension = null; /** * A constructor that constructs {@code BaseUnit} instances. - * + * * @since 2019-10-16 + * @since v0.3.0 */ Unit(final NameSymbol nameSymbol) { - if (this instanceof BaseUnit) { - this.unitBase = ObjectProduct.oneOf((BaseUnit) this); - } else + if (!(this instanceof BaseUnit)) throw new AssertionError(); + this.unitBase = ObjectProduct.oneOf((BaseUnit) this); this.nameSymbol = nameSymbol; } /** * Creates the {@code Unit}. - * + * * @param unitBase base of unit * @param ns names and symbol of unit * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if unitBase or ns is null */ protected Unit(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { @@ -137,18 +144,9 @@ public abstract class Unit implements Nameable { } /** - * @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 or unitlike form to test with * @return true if they are compatible * @since 2019-01-13 @@ -161,21 +159,6 @@ public abstract class Unit implements Nameable { } /** - * 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. * <p> @@ -187,10 +170,10 @@ public abstract class Unit implements Nameable { * 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}. - * + * * @param value value expressed in <b>base</b> unit * @return value expressed in <b>this</b> unit * @since 2018-12-22 @@ -201,16 +184,17 @@ public abstract class Unit implements Nameable { /** * 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. - * + * * @param other unit to convert to * @param value value to convert * @return converted value * @since 2019-05-22 + * @since v0.3.0 * @throws IllegalArgumentException if {@code other} is incompatible for * conversion with this unit (as tested by * {@link Unit#canConvertTo}). @@ -220,37 +204,8 @@ public abstract class Unit implements Nameable { 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 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)); + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); } /** @@ -265,10 +220,10 @@ public abstract class Unit implements Nameable { * 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}. - * + * * @param value value expressed in <b>this</b> unit * @return value expressed in <b>base</b> unit * @since 2018-12-22 @@ -292,7 +247,7 @@ public abstract class Unit implements Nameable { */ public final ObjectProduct<BaseDimension> getDimension() { if (this.dimension == null) { - final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); + final var mapping = this.unitBase.exponentMap(); final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); for (final BaseUnit key : mapping.keySet()) { @@ -307,6 +262,7 @@ public abstract class Unit implements Nameable { /** * @return the nameSymbol * @since 2020-09-07 + * @since v0.3.0 */ @Override public final NameSymbol getNameSymbol() { @@ -314,7 +270,7 @@ public abstract class Unit implements Nameable { } /** - * Returns true iff this unit is metric. + * Determines whether this unit is metric. * <p> * "Metric" is defined by three conditions: * <ul> @@ -329,14 +285,17 @@ public abstract class Unit implements Nameable { * <p> * All SI units (as designated by the BIPM) except the degree Celsius are * considered "metric" by this definition. - * + * + * @return true iff this unit is metric. + * * @since 2020-08-27 + * @since v0.3.0 */ public final boolean isMetric() { // first condition - check that it is a linear unit if (!(this instanceof LinearUnit)) return false; - final LinearUnit linear = (LinearUnit) this; + final var linear = (LinearUnit) this; // second condition - check that for (final BaseUnit b : linear.getBase().getBaseSet()) { @@ -352,18 +311,18 @@ public abstract class Unit implements Nameable { /** * @return a string representing this unit's definition * @since 2022-03-10 + * @since v0.3.0 */ public String toDefinitionString() { if (!this.unitBase.getNameSymbol().isEmpty()) return "derived from " + this.unitBase.getName(); - else - return "derived from " - + this.getBase().toString(BaseUnit::getShortName); + return "derived from " + this.getBase().toString(BaseUnit::getShortName); } /** * @return a string containing both this unit's name and its definition * @since 2022-03-10 + * @since v0.3.0 */ public final String toFullString() { return this.toString() + " (" + this.toDefinitionString() + ")"; @@ -375,14 +334,14 @@ public abstract class Unit implements Nameable { && this.nameSymbol.getSymbol().isPresent()) return this.nameSymbol.getPrimaryName().orElseThrow() + " (" + this.nameSymbol.getSymbol().orElseThrow() + ")"; - else - return this.getName(); + return this.getName(); } /** * @param ns name(s) and symbol to use * @return a copy of this unit with provided name(s) and symbol * @since 2019-10-21 + * @since v0.3.0 * @throws NullPointerException if ns is null */ public Unit withName(final NameSymbol ns) { diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 0120067..36c225f 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2024 Adrien Hopkins + * Copyright (C) 2018-2025 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 @@ -40,11 +40,9 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; -import java.util.regex.Matcher; import java.util.regex.Pattern; import sevenUnits.utils.ConditionalExistenceCollections; -import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; @@ -52,7 +50,7 @@ import sevenUnits.utils.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. - * + * * @author Adrien Hopkins * @since 2019-01-07 * @since v0.1.0 @@ -89,7 +87,7 @@ public final class UnitDatabase { * Because of ambiguities between prefixes (i.e. kilokilo = mega), * {@link #containsValue} and {@link #values()} currently ignore prefixes. * </p> - * + * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 @@ -97,7 +95,7 @@ public final class UnitDatabase { private static final class PrefixedUnitMap implements Map<String, Unit> { /** * 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 @@ -105,7 +103,7 @@ public final class UnitDatabase { * {@code IllegalStateException} instead of creating an infinite-sized * array. * </p> - * + * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 @@ -114,7 +112,7 @@ public final class UnitDatabase { extends AbstractSet<Map.Entry<String, Unit>> { /** * The entry for this set. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -126,7 +124,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitEntry}. - * + * * @param key key * @param value value * @since 2019-04-14 @@ -139,6 +137,7 @@ public final class UnitDatabase { /** * @since 2019-05-03 + * @since v0.3.0 */ @Override public boolean equals(final Object o) { @@ -161,6 +160,7 @@ public final class UnitDatabase { /** * @since 2019-05-03 + * @since v0.3.0 */ @Override public int hashCode() { @@ -180,8 +180,9 @@ public final class UnitDatabase { * string is the string representation of the key, then the equals * ({@code =}) character, then the string representation of the * value. - * + * * @since 2019-05-03 + * @since v0.3.0 */ @Override public String toString() { @@ -192,7 +193,7 @@ public final class UnitDatabase { /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -212,7 +213,9 @@ public final class UnitDatabase { /** * Creates the * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * + * + * @param map map to base iterator on + * * @since 2019-04-14 * @since v0.2.0 */ @@ -228,7 +231,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); + final var unitName = new StringBuilder(); for (final int i : this.prefixCoordinates) { unitName.append(this.prefixNames.get(i)); } @@ -241,18 +244,15 @@ public final class UnitDatabase { public boolean hasNext() { if (this.unitNames.isEmpty()) return false; - else { - if (this.prefixNames.isEmpty()) - return this.prefixCoordinates.isEmpty() - && this.unitNamePosition < this.unitNames.size(); - else - return true; - } + if (this.prefixNames.isEmpty()) + return this.prefixCoordinates.isEmpty() + && this.unitNamePosition < this.unitNames.size(); + return true; } /** * Changes this iterator's position to the next available one. - * + * * @since 2019-04-14 * @since v0.2.0 */ @@ -268,7 +268,7 @@ public final class UnitDatabase { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; + var i = this.prefixCoordinates.size() - 1; this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); @@ -294,7 +294,7 @@ public final class UnitDatabase { @Override public Entry<String, Unit> next() { // get next element - final Entry<String, Unit> nextEntry = this.peek(); + final var nextEntry = this.peek(); // iterate to next position this.incrementPosition(); @@ -306,6 +306,7 @@ public final class UnitDatabase { * @return the next element in the iterator, without iterating over * it * @since 2019-05-03 + * @since v0.3.0 */ private Entry<String, Unit> peek() { if (!this.hasNext()) @@ -322,7 +323,7 @@ public final class UnitDatabase { } } - final String nextName = this.getCurrentUnitName(); + final var nextName = this.getCurrentUnitName(); return new PrefixedUnitEntry(nextName, this.map.get(nextName)); } @@ -330,8 +331,9 @@ public final class UnitDatabase { /** * Returns a string representation of the object. The exact details * of the representation are unspecified and subject to change. - * + * * @since 2019-05-03 + * @since v0.3.0 */ @Override public String toString() { @@ -346,7 +348,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitNameSet}. - * + * * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 @@ -383,7 +385,7 @@ public final class UnitDatabase { // 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; + final var tempEntry = (Entry<String, Unit>) o; entry = tempEntry; } catch (final ClassCastException e) { throw new IllegalArgumentException( @@ -441,55 +443,45 @@ public final class UnitDatabase { public int size() { if (this.map.units.isEmpty()) return 0; - else { - if (this.map.prefixes.isEmpty()) - return this.map.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + // infinite set + 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() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + 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) { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(a); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + 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", - this.map.units, this.map.prefixes); + 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 @@ -497,7 +489,7 @@ public final class UnitDatabase { * {@code IllegalStateException} instead of creating an infinite-sized * array. * </p> - * + * * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 @@ -507,7 +499,7 @@ public final class UnitDatabase { /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. - * + * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 @@ -527,7 +519,9 @@ public final class UnitDatabase { /** * Creates the * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * + * + * @param map map to base itorator on + * * @since 2019-04-14 * @since v0.2.0 */ @@ -543,7 +537,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); + final var unitName = new StringBuilder(); for (final int i : this.prefixCoordinates) { unitName.append(this.prefixNames.get(i)); } @@ -556,18 +550,15 @@ public final class UnitDatabase { public boolean hasNext() { if (this.unitNames.isEmpty()) return false; - else { - if (this.prefixNames.isEmpty()) - return this.prefixCoordinates.isEmpty() - && this.unitNamePosition < this.unitNames.size(); - else - return true; - } + if (this.prefixNames.isEmpty()) + return this.prefixCoordinates.isEmpty() + && this.unitNamePosition < this.unitNames.size(); + return true; } /** * Changes this iterator's position to the next available one. - * + * * @since 2019-04-14 * @since v0.2.0 */ @@ -583,7 +574,7 @@ public final class UnitDatabase { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; + var i = this.prefixCoordinates.size() - 1; this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); @@ -608,7 +599,7 @@ public final class UnitDatabase { @Override public String next() { - final String nextName = this.peek(); + final var nextName = this.peek(); this.incrementPosition(); @@ -619,6 +610,7 @@ public final class UnitDatabase { * @return the next element in the iterator, without iterating over * it * @since 2019-05-03 + * @since v0.3.0 */ private String peek() { if (!this.hasNext()) @@ -640,8 +632,9 @@ public final class UnitDatabase { /** * Returns a string representation of the object. The exact details * of the representation are unspecified and subject to change. - * + * * @since 2019-05-03 + * @since v0.3.0 */ @Override public String toString() { @@ -656,7 +649,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitNameSet}. - * + * * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 @@ -734,56 +727,46 @@ public final class UnitDatabase { public int size() { if (this.map.units.isEmpty()) return 0; - else { - if (this.map.prefixes.isEmpty()) - return this.map.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + // infinite set + 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() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + 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) { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(a); - else - // infinite set - throw new IllegalStateException( - "Cannot make an infinite set into an array."); + // infinite set + 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", - this.map.units, this.map.prefixes); + 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. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -791,7 +774,7 @@ public final class UnitDatabase { /** * The available prefixes for use. - * + * * @since 2019-04-13 * @since v0.2.0 */ @@ -804,7 +787,7 @@ public final class UnitDatabase { /** * Creates the {@code PrefixedUnitMap}. - * + * * @param units map mapping unit names to units * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 @@ -855,11 +838,11 @@ public final class UnitDatabase { if (!(key instanceof String)) throw new IllegalArgumentException( "Attempted to test for a unit using a non-string name."); - final String unitName = (String) key; + final var unitName = (String) key; // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; - int longestLength = 0; + var longestLength = 0; for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: @@ -871,7 +854,7 @@ public final class UnitDatabase { // linear units can have prefixes) if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); + final var rest = unitName.substring(prefixName.length()); if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; @@ -885,7 +868,7 @@ public final class UnitDatabase { /** * {@inheritDoc} - * + * * <p> * Because of ambiguities between prefixes (i.e. kilokilo = mega), this * method only tests for prefixless units. @@ -914,11 +897,11 @@ public final class UnitDatabase { if (!(key instanceof String)) throw new IllegalArgumentException( "Attempted to obtain a unit using a non-string name."); - final String unitName = (String) key; + final var unitName = (String) key; // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; - int longestLength = 0; + var longestLength = 0; for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: @@ -930,7 +913,7 @@ public final class UnitDatabase { // linear units can have prefixes) if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); + final var rest = unitName.substring(prefixName.length()); if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; @@ -942,16 +925,14 @@ public final class UnitDatabase { // 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 - final LinearUnit unit = (LinearUnit) this.get(rest); - final UnitPrefix prefix = this.prefixes.get(longestPrefix); - - return unit.withPrefix(prefix); - } + // get necessary data + final var rest = unitName.substring(longestLength); + // this cast will not fail because I verified that it would work + // before selecting this prefix + final var unit = (LinearUnit) this.get(rest); + final var prefix = this.prefixes.get(longestPrefix); + + return unit.withPrefix(prefix); } @Override @@ -1028,28 +1009,24 @@ public final class UnitDatabase { public int size() { if (this.units.isEmpty()) return 0; - else { - if (this.prefixes.isEmpty()) - return this.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } + if (this.prefixes.isEmpty()) + return this.units.size(); + // infinite set + return Integer.MAX_VALUE; } @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) return new HashMap<>(this).toString(); - else - return String.format( - "Infinite map of name-unit entries created from units %s and prefixes %s", - this.units, this.prefixes); + 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. @@ -1066,34 +1043,6 @@ public final class UnitDatabase { } /** - * Replacements done to *all* expression types - */ - private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>(); - - // add data to expression replacements - static { - // 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 - + "\\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") - ), "\\($1 $2\\)"); - } - - /** * A regular expression that separates names and expressions in unit files. */ private static final Pattern NAME_EXPRESSION = Pattern @@ -1108,66 +1057,76 @@ public final class UnitDatabase { /** * The exponent operator - * + * * @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, + private static LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { - // exponent function - first check if o2 is a number, - if (exponentUnit.getBase().equals(Metric.ONE.getBase())) { - // then check if it is an integer, - final double exponent = exponentUnit.getConversionFactor(); - 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."); + if (!exponentUnit.getBase().equals(Metric.ONE.getBase())) + throw new IllegalArgumentException(String.format( + "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.", + base, exponentUnit)); + + final var exponent = exponentUnit.getConversionFactor(); + return base.toExponentRounded(exponent); } /** * The exponent operator - * + * * @param base base of exponentiation * @param exponentUnit exponent * @return result * @since 2020-08-04 + * @since v0.3.0 */ - private static final LinearUnitValue exponentiateUnitValues( + private static LinearUnitValue exponentiateUnitValues( final LinearUnitValue base, final LinearUnitValue exponentValue) { - // exponent function - first check if o2 is a number, - if (exponentValue.canConvertTo(Metric.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."); + if (!exponentValue.canConvertTo(Metric.ONE)) + throw new IllegalArgumentException(String.format( + "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.", + base, exponentValue)); + + final var exponent = exponentValue.getValueExact(); + return base.toExponentRounded(exponent); + } + + /** + * Formats an expression so it can be parsed by the expression parser. + * + * Specifically, puts spaces around all operators so they can be parsed as + * words. + * + * @param expression expression to format + * @return formatted expression + * @since 2025-06-07 + * @since v1.0.0 + */ + static String formatExpression(String expression) { + var modifiedExpression = expression; + for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) { + modifiedExpression = modifiedExpression.replaceAll(operator, + " " + operator + " "); + } + + modifiedExpression = modifiedExpression.replaceAll("\\s+", " "); + return modifiedExpression; } /** * @return true if entry represents a removable duplicate entry of map. * @since 2021-05-22 + * @since v0.3.0 */ static <T> boolean isRemovableDuplicate(Map<String, T> map, Entry<String, T> entry) { for (final Entry<String, T> e : map.entrySet()) { - final String name = e.getKey(); - final T value = e.getValue(); + final var name = e.getKey(); + final var value = e.getValue(); if (lengthFirstComparator.compare(entry.getKey(), name) < 0 && Objects.equals(map.get(entry.getKey()), value)) return true; @@ -1177,7 +1136,7 @@ public final class UnitDatabase { /** * The units in this system, excluding prefixes. - * + * * @since 2019-01-07 * @since v0.1.0 */ @@ -1185,7 +1144,7 @@ public final class UnitDatabase { /** * The unit prefixes in this system. - * + * * @since 2019-01-14 * @since v0.1.0 */ @@ -1193,7 +1152,7 @@ public final class UnitDatabase { /** * The dimensions in this system. - * + * * @since 2019-03-14 * @since v0.2.0 */ @@ -1201,13 +1160,21 @@ public final class UnitDatabase { /** * A map mapping strings to units (including prefixes) - * + * * @since 2019-04-13 * @since v0.2.0 */ private final Map<String, Unit> units; /** + * A map mapping strings to unit sets + * + * @since 2024-08-16 + * @since v1.0.0 + */ + private final Map<String, List<LinearUnit>> unitSets; + + /** * 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> @@ -1221,71 +1188,72 @@ public final class UnitDatabase { /** * A parser that can parse unit expressions. - * + * * @since 2019-03-22 * @since v0.2.0 */ 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.dividedBy(o2), 1) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); + this::getLinearUnit).addBinaryOperator("+", LinearUnit::plus, 0) + .addBinaryOperator("-", LinearUnit::minus, 0) + .addBinaryOperator("*", LinearUnit::times, 1) + .addBinaryOperator("space_times", LinearUnit::times, 2) + .addSpaceFunction("space_times") + .addBinaryOperator("/", LinearUnit::dividedBy, 1) + .addBinaryOperator("|", LinearUnit::dividedBy, 4) + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 3).build(); /** * A parser that can parse unit value expressions. - * + * * @since 2020-08-04 + * @since v0.3.0 */ 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("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .addBinaryOperator("+", LinearUnitValue::plus, 0) + .addBinaryOperator("-", LinearUnitValue::minus, 0) + .addBinaryOperator("*", LinearUnitValue::times, 1) + .addBinaryOperator("space_times", LinearUnitValue::times, 2) + .addSpaceFunction("space_times") + .addBinaryOperator("/", LinearUnitValue::dividedBy, 1) + .addBinaryOperator("|", LinearUnitValue::dividedBy, 4) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 3) .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.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("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), - 2) + this::getPrefix).addBinaryOperator("+", UnitPrefix::plus, 0) + .addBinaryOperator("-", UnitPrefix::minus, 0) + .addBinaryOperator("*", UnitPrefix::times, 1).addSpaceFunction("*") + .addBinaryOperator("/", UnitPrefix::dividedBy, 1) + .addBinaryOperator("|", UnitPrefix::dividedBy, 3).addBinaryOperator( + "^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 2) .build(); /** * A parser that can parse unit dimension expressions. - * + * * @since 2019-04-13 * @since v0.2.0 */ private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + this::getDimension).addBinaryOperator("*", ObjectProduct::times, 0) .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2) + .addBinaryOperator("/", ObjectProduct::dividedBy, 0) + .addBinaryOperator("|", ObjectProduct::dividedBy, 2) .addNumericOperator("^", (o1, o2) -> { - int exponent = (int) Math.round(o2.value()); + final var exponent = (int) Math.round(o2.value()); return o1.toExponent(exponent); }, 1).build(); /** * Creates the {@code UnitsDatabase}. - * + * * @since 2019-01-10 * @since v0.1.0 */ @@ -1295,10 +1263,11 @@ public final class UnitDatabase { /** * Creates the {@code UnitsDatabase} - * + * * @param prefixRepetitionRule the rule that determines when prefix * repetition is allowed * @since 2020-08-26 + * @since v0.3.0 */ public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) { this.prefixlessUnits = new HashMap<>(); @@ -1309,11 +1278,12 @@ public final class UnitDatabase { new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), entry -> this.prefixRepetitionRule .test(this.getPrefixesFromName(entry.getKey()))); + this.unitSets = new HashMap<>(); } /** * 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 @@ -1324,14 +1294,14 @@ public final class UnitDatabase { final ObjectProduct<BaseDimension> dimension) { Objects.requireNonNull(name, "name may not be null"); Objects.requireNonNull(dimension, "dimension may not be null"); - final ObjectProduct<BaseDimension> namedDimension = dimension + final var namedDimension = dimension .withName(dimension.getNameSymbol().withExtraName(name)); this.dimensions.put(name, namedDimension); } /** * 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 * @since 2019-04-10 @@ -1349,13 +1319,13 @@ public final class UnitDatabase { } // divide line into name and expression - final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + final var lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) throw new IllegalArgumentException(String.format( "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.", lineCounter)); - final String name = lineMatcher.group(1); - final String expression = lineMatcher.group(2); + final var name = lineMatcher.group(1); + final var expression = lineMatcher.group(2); // if (name.endsWith(" ")) { // System.err.printf("Warning - line %d's dimension name ends in a space", @@ -1369,22 +1339,13 @@ public final class UnitDatabase { 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; - try { - dimension = this.getDimensionFromExpression(expression); - } catch (final IllegalArgumentException | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addDimension(name, dimension); + this.addDimension(name, this.getDimensionFromExpression(expression)); } } /** * 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 @@ -1401,7 +1362,7 @@ public final class UnitDatabase { /** * Adds a unit to the database. - * + * * @param name unit's name * @param unit unit to add * @throws NullPointerException if unit is null @@ -1418,7 +1379,7 @@ public final class UnitDatabase { /** * 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 * @since 2019-04-10 @@ -1436,14 +1397,14 @@ public final class UnitDatabase { } // divide line into name and expression - final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + final var lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) throw new IllegalArgumentException(String.format( "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 var name = lineMatcher.group(1); - final String expression = lineMatcher.group(2); + final var expression = lineMatcher.group(2); // this code should never occur // if (name.endsWith(" ")) { @@ -1457,47 +1418,55 @@ public final class UnitDatabase { if (!this.containsUnitName(name)) throw new IllegalArgumentException(String .format("! used but no unit found (line %d).", lineCounter)); + } else if (name.endsWith("-")) { + final var prefixName = name.substring(0, name.length() - 1); + this.addPrefix(prefixName, this.getPrefixFromExpression(expression)); + } else if (expression.contains(";")) { + // it's a multi-unit + this.addUnitSet(name, this.getUnitSetFromExpression(expression)); } else { - if (name.endsWith("-")) { - final UnitPrefix prefix; - try { - prefix = this.getPrefixFromExpression(expression); - } catch (final IllegalArgumentException - | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - final String prefixName = name.substring(0, name.length() - 1); - this.addPrefix(prefixName, prefix); - } else { - // it's a unit, get the unit - final Unit unit; - try { - unit = this.getUnitFromExpression(expression); - } catch (final IllegalArgumentException - | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - this.addUnit(name, unit); - } + // it's a unit, get the unit + this.addUnit(name, this.getUnitFromExpression(expression)); } } /** - * Removes all units, prefixes and dimensions from this database. - * + * Add a unit set to the database. + * + * @param name name of unit set + * @param value unit set to add + * @since 2024-08-16 + * @since v1.0.0 + */ + public void addUnitSet(String name, List<LinearUnit> value) { + if (value.isEmpty()) + throw new IllegalArgumentException("Unit sets must not be empty."); + for (final LinearUnit unit : value.subList(1, value.size())) { + if (!Objects.equals(unit.getDimension(), value.get(0).getDimension())) + throw new IllegalArgumentException( + "Unit sets must be all the same dimension, " + value + + " is not."); + } + + this.unitSets.put(name, value); + } + + /** + * Removes all units, unit sets, prefixes and dimensions from this database. + * * @since 2022-02-26 + * @since v0.4.0 */ public void clear() { this.dimensions.clear(); this.prefixes.clear(); this.prefixlessUnits.clear(); + this.unitSets.clear(); } /** * Tests if the database has a unit dimension with this name. - * + * * @param name name to test * @return if database contains name * @since 2019-03-14 @@ -1509,7 +1478,7 @@ public final class UnitDatabase { /** * Tests if the database has a unit prefix with this name. - * + * * @param name name to test * @return if database contains name * @since 2019-01-13 @@ -1522,7 +1491,7 @@ public final class UnitDatabase { /** * Tests if the database has a unit with this name, taking prefixes into * consideration - * + * * @param name name to test * @return if database contains name * @since 2019-01-13 @@ -1533,6 +1502,19 @@ public final class UnitDatabase { } /** + * Returns true iff there is a unit set with this name. + * + * @param name name to check for + * @return true iff there is a unit set with this name + * + * @since 2024-08-16 + * @since v1.0.0 + */ + public boolean containsUnitSetName(String name) { + return this.unitSets.containsKey(name); + } + + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 * @since v0.2.0 @@ -1544,10 +1526,11 @@ public final class UnitDatabase { /** * 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 + * @since v0.3.0 */ public LinearUnitValue evaluateUnitExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); @@ -1556,37 +1539,13 @@ public final class UnitDatabase { 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); + return this.unitValueExpressionParser + .parseExpression(formatExpression(expression)); } /** * Gets a unit dimension from the database using its name. - * + * * @param name dimension's name * @return dimension * @since 2019-03-14 @@ -1594,12 +1553,11 @@ public final class UnitDatabase { */ public ObjectProduct<BaseDimension> getDimension(final String name) { Objects.requireNonNull(name, "name must not be null."); - final ObjectProduct<BaseDimension> dimension = this.dimensions.get(name); + final var dimension = this.dimensions.get(name); if (dimension == null) throw new NoSuchElementException( "No dimension with name \"" + name + "\"."); - else - return dimension; + return dimension; } /** @@ -1614,8 +1572,9 @@ public final class UnitDatabase { * multiplication)</li> * <li>The operator '^' which exponentiates. Exponents must be integers.</li> * </ul> - * + * * @param expression expression to parse + * @return parsed unit dimension * @throws IllegalArgumentException if the expression cannot be parsed * @throws NullPointerException if expression is null * @since 2019-04-13 @@ -1629,23 +1588,14 @@ public final class UnitDatabase { if (this.containsDimensionName(expression)) return this.getDimension(expression); - // force operators to have spaces - String modifiedExpression = expression; - - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - return this.unitDimensionParser.parseExpression(modifiedExpression); + return this.unitDimensionParser + .parseExpression(formatExpression(expression)); } /** * 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 * @return unit * @since 2019-03-22 @@ -1662,29 +1612,28 @@ public final class UnitDatabase { "Format nonlinear units like: unit(value)."); // solve the function - final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble( + final var unit = this.getUnit(parts.get(0)); + final var 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)); } + // get a linear unit + final var unit = this.getUnit(name); + + if (unit instanceof LinearUnit) + return (LinearUnit) unit; + 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 + * @since v0.3.0 */ LinearUnitValue getLinearUnitValue(final String name) { try { @@ -1698,7 +1647,7 @@ public final class UnitDatabase { /** * Gets a unit prefix from the database from its name - * + * * @param name prefix's name * @return prefix * @since 2019-01-10 @@ -1708,30 +1657,30 @@ public final class UnitDatabase { try { return UnitPrefix.valueOf(Double.parseDouble(name)); } catch (final NumberFormatException e) { - final UnitPrefix prefix = this.prefixes.get(name); + final var prefix = this.prefixes.get(name); if (prefix == null) throw new NoSuchElementException( "No prefix with name \"" + name + "\"."); - else - return prefix; + return prefix; } } /** * 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 + * @since v0.3.0 */ List<UnitPrefix> getPrefixesFromName(final String unitName) { final List<UnitPrefix> prefixes = new ArrayList<>(); - String name = unitName; + var name = unitName; while (!this.prefixlessUnits.containsKey(name)) { // find the longest prefix String longestPrefixName = null; - int longestLength = name.length(); + var longestLength = name.length(); while (longestPrefixName == null) { longestLength--; @@ -1744,7 +1693,7 @@ public final class UnitDatabase { } // longest prefix found! - final UnitPrefix prefix = this.getPrefix(longestPrefixName); + final var prefix = this.getPrefix(longestPrefixName); prefixes.add(0, prefix); name = name.substring(longestLength); } @@ -1757,7 +1706,7 @@ public final class UnitDatabase { * 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 * @return prefix * @throws IllegalArgumentException if expression cannot be parsed @@ -1772,30 +1721,22 @@ public final class UnitDatabase { if (this.containsUnitName(expression)) return this.getPrefix(expression); - // force operators to have spaces - String modifiedExpression = expression; - - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - return this.prefixExpressionParser.parseExpression(modifiedExpression); + return this.prefixExpressionParser + .parseExpression(formatExpression(expression)); } /** * @return the prefixRepetitionRule * @since 2020-08-26 + * @since v0.3.0 */ - public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { + public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { return this.prefixRepetitionRule; } /** * Gets a unit from the database from its name, looking for prefixes. - * + * * @param name unit's name * @return unit * @since 2019-01-10 @@ -1803,27 +1744,28 @@ public final class UnitDatabase { */ public Unit getUnit(final String name) { try { - final double value = Double.parseDouble(name); + final var value = Double.parseDouble(name); return Metric.ONE.times(value); } catch (final NumberFormatException e) { - final Unit unit = this.units.get(name); + final var unit = this.units.get(name); if (unit == null) throw new NoSuchElementException("No unit " + name); - else if (unit.getPrimaryName().isEmpty()) + if (unit.getPrimaryName().isEmpty()) return unit.withName(NameSymbol.ofName(name)); - else if (!unit.getPrimaryName().get().equals(name)) { + 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)) { + } + 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; + } + return unit; } } @@ -1842,8 +1784,9 @@ public final class UnitDatabase { * <li>A number which is multiplied or divided</li> * </ul> * This method only works with linear units. - * + * * @param expression expression to parse + * @return parsed unit * @throws IllegalArgumentException if the expression cannot be parsed * @throws NullPointerException if expression is null * @since 2019-01-07 @@ -1856,31 +1799,51 @@ public final class UnitDatabase { if (this.containsUnitName(expression)) return this.getUnit(expression); - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); - modifiedExpression = modifiedExpression.replaceAll("-", " - "); + return this.unitExpressionParser + .parseExpression(formatExpression(expression)); + } - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } + /** + * Get a unit set from its name, throwing a {@link NoSuchElementException} if + * there is none. + * + * @param name name of unit set + * @return unit set with that name + * + * @since 2024-08-16 + * @since v1.0.0 + */ + public List<LinearUnit> getUnitSet(String name) { + final var unitSet = this.unitSets.get(name); + if (unitSet == null) + throw new NoSuchElementException("No unit set with name " + name); + return unitSet; + } - // 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)))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) - + modifiedExpression.substring(i + 2); - } - } + /** + * Parses a semicolon-separated expression to get the unit set being used. + * + * @since 2024-08-22 + * @since v1.0.0 + */ + List<LinearUnit> getUnitSetFromExpression(String expression) { + final var parts = expression.split(";"); + final List<LinearUnit> units = new ArrayList<>(parts.length); + for (final String unitName : parts) { + final var unit = this.getUnitFromExpression(unitName.trim()); - return this.unitExpressionParser.parseExpression(modifiedExpression); + if (!(unit instanceof LinearUnit)) + throw new IllegalArgumentException(String.format( + "Unit '%s' is in a unit-set expression, but is not linear.", + unitName)); + if (units.size() > 0 && !unit.canConvertTo(units.get(0))) + throw new IllegalArgumentException(String.format( + "Units in expression '%s' have different dimensions.", + expression)); + + units.add((LinearUnit) unit); + } + return units; } /** @@ -1901,26 +1864,32 @@ public final class UnitDatabase { * 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 + * @throws NullPointerException if file is null + * @return list of errors that happened when loading file * @since 2019-01-13 * @since v0.1.0 */ - public void loadDimensionFile(final Path file) { + public List<LoadingException> loadDimensionFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); + final List<LoadingException> errors = new ArrayList<>(); try { - long lineCounter = 0; + var lineCounter = 0L; for (final String line : Files.readAllLines(file)) { - this.addDimensionFromLine(line, ++lineCounter); + try { + this.addDimensionFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, file, + LoadingException.FileType.DIMENSION, e)); + } } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); } catch (final IOException e) { throw new IllegalArgumentException("Could not read file " + file, e); } + return errors; } /** @@ -1928,15 +1897,26 @@ public final class UnitDatabase { * {@link #loadDimensionFile}. * * @param stream stream to load from + * @return list of all errors that happened loading the stream * @since 2021-03-27 + * @since v0.3.0 */ - public void loadDimensionsFromStream(final InputStream stream) { - try (final Scanner scanner = new Scanner(stream)) { - long lineCounter = 0; + public List<LoadingException> loadDimensionsFromStream( + final InputStream stream) { + final List<LoadingException> errors = new ArrayList<>(); + try (final var scanner = new Scanner(stream)) { + var lineCounter = 0L; while (scanner.hasNextLine()) { - this.addDimensionFromLine(scanner.nextLine(), ++lineCounter); + final var line = scanner.nextLine(); + try { + this.addDimensionFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, + LoadingException.FileType.DIMENSION, e)); + } } } + return errors; } /** @@ -1956,26 +1936,32 @@ public final class UnitDatabase { * 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 + * @throws NullPointerException if file is null + * @return list of errors that happened when loading file * @since 2019-01-13 * @since v0.1.0 */ - public void loadUnitsFile(final Path file) { + public List<LoadingException> loadUnitsFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); + final List<LoadingException> errors = new ArrayList<>(); try { - long lineCounter = 0; + var lineCounter = 0L; for (final String line : Files.readAllLines(file)) { - this.addUnitOrPrefixFromLine(line, ++lineCounter); + try { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, file, + LoadingException.FileType.UNIT, e)); + } } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); } catch (final IOException e) { throw new IllegalArgumentException("Could not read file " + file, e); } + return errors; } /** @@ -1983,15 +1969,25 @@ public final class UnitDatabase { * {@link #loadUnitsFile}. * * @param stream stream to load from + * @return list of all errors that happened loading the stream * @since 2021-03-27 + * @since v0.3.0 */ - public void loadUnitsFromStream(InputStream stream) { - try (final Scanner scanner = new Scanner(stream)) { - long lineCounter = 0; + public List<LoadingException> loadUnitsFromStream(InputStream stream) { + final List<LoadingException> errors = new ArrayList<>(); + try (final var scanner = new Scanner(stream)) { + var lineCounter = 0L; while (scanner.hasNextLine()) { - this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter); + final var line = scanner.nextLine(); + try { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, + LoadingException.FileType.UNIT, e)); + } } } + return errors; } /** @@ -2003,17 +1999,17 @@ public final class UnitDatabase { public Map<String, UnitPrefix> prefixMap(boolean includeDuplicates) { if (includeDuplicates) return Collections.unmodifiableMap(this.prefixes); - else - return Collections.unmodifiableMap(ConditionalExistenceCollections - .conditionalExistenceMap(this.prefixes, - entry -> !isRemovableDuplicate(this.prefixes, entry))); + return Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(this.prefixes, + entry -> !isRemovableDuplicate(this.prefixes, entry))); } /** * @param prefixRepetitionRule the prefixRepetitionRule to set * @since 2020-08-26 + * @since v0.3.0 */ - public final void setPrefixRepetitionRule( + public void setPrefixRepetitionRule( Predicate<List<UnitPrefix>> prefixRepetitionRule) { this.prefixRepetitionRule = prefixRepetitionRule; } @@ -2041,18 +2037,18 @@ public final class UnitDatabase { * <p> * Specifically, the operations that will throw an IllegalStateException if * the map is infinite in size are: + * </p> * <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. * </p> - * + * * @return a map mapping unit names to units, including prefixed names * @since 2019-04-13 * @since v0.2.0 @@ -2073,10 +2069,17 @@ public final class UnitDatabase { public Map<String, Unit> unitMapPrefixless(boolean includeDuplicates) { if (includeDuplicates) return Collections.unmodifiableMap(this.prefixlessUnits); - else - return Collections.unmodifiableMap(ConditionalExistenceCollections - .conditionalExistenceMap(this.prefixlessUnits, - entry -> !isRemovableDuplicate(this.prefixlessUnits, - entry))); + return Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(this.prefixlessUnits, + entry -> !isRemovableDuplicate(this.prefixlessUnits, entry))); + } + + /** + * @return an unmodifiable map mapping names to unit sets + * @since 2024-08-16 + * @since v1.0.0 + */ + public Map<String, List<LinearUnit>> unitSetMap() { + return Collections.unmodifiableMap(this.unitSets); } } diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index 9035969..af106b9 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -25,17 +25,19 @@ import sevenUnits.utils.Nameable; /** * A prefix that can be applied to a {@code LinearUnit} to multiply it by some * value - * + * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.3.0 */ public final class UnitPrefix implements Nameable { /** * Gets a {@code UnitPrefix} from a multiplier - * + * * @param multiplier multiplier of prefix * @return prefix * @since 2019-10-16 + * @since v0.3.0 */ public static UnitPrefix valueOf(final double multiplier) { return new UnitPrefix(multiplier, NameSymbol.EMPTY); @@ -43,11 +45,12 @@ public final class UnitPrefix implements Nameable { /** * Gets a {@code UnitPrefix} from a multiplier and a name - * + * * @param multiplier multiplier of prefix * @param ns name(s) and symbol of prefix * @return prefix * @since 2019-10-16 + * @since v0.3.0 * @throws NullPointerException if ns is null */ public static UnitPrefix valueOf(final double multiplier, @@ -58,21 +61,23 @@ public final class UnitPrefix implements Nameable { /** * This prefix's name(s) and symbol. - * + * * @since 2022-04-16 + * @since v0.4.0 */ private final NameSymbol nameSymbol; /** * The number that this prefix multiplies units by - * + * * @since 2019-10-16 + * @since v0.3.0 */ private final double multiplier; /** * Creates the {@code DefaultUnitPrefix}. - * + * * @param multiplier * @since 2019-01-14 * @since v0.2.0 @@ -84,10 +89,11 @@ public final class UnitPrefix implements Nameable { /** * Divides this prefix by a scalar - * + * * @param divisor number to divide by * @return quotient of prefix and scalar * @since 2019-10-16 + * @since v0.3.0 */ public UnitPrefix dividedBy(final double divisor) { return valueOf(this.getMultiplier() / divisor); @@ -95,7 +101,7 @@ public final class UnitPrefix implements Nameable { /** * Divides this prefix by {@code other}. - * + * * @param other prefix to divide by * @return quotient of prefixes * @since 2019-04-13 @@ -107,18 +113,32 @@ public final class UnitPrefix implements Nameable { /** * {@inheritDoc} - * + * * Uses the prefix's multiplier to determine equality. */ @Override public boolean equals(final Object obj) { if (this == obj) return true; - if (obj == null) + if ((obj == null) || !(obj instanceof UnitPrefix)) return false; - if (!(obj instanceof UnitPrefix)) + final var other = (UnitPrefix) obj; + return Double.compare(this.getMultiplier(), other.getMultiplier()) == 0; + } + + /** + * @param other prefix to compare to + * @return true iff this prefix and other are equal, ignoring small + * differences caused by floating-point error. + * + * @apiNote This method is not transitive, so it cannot be used as an equals + * method. + */ + public boolean equalsApproximately(final UnitPrefix other) { + if (this == other) + return true; + if (other == null) return false; - final UnitPrefix other = (UnitPrefix) obj; return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); } @@ -126,6 +146,7 @@ public final class UnitPrefix implements Nameable { /** * @return prefix's multiplier * @since 2019-11-26 + * @since v0.3.0 */ public double getMultiplier() { return this.multiplier; @@ -138,46 +159,55 @@ public final class UnitPrefix implements Nameable { /** * {@inheritDoc} - * + * * Uses the prefix's multiplier to determine a hash code. */ @Override public int hashCode() { - return DecimalComparison.hash(this.getMultiplier()); + return Double.hashCode(this.getMultiplier()); } /** - * Multiplies this prefix by a scalar - * - * @param multiplicand number to multiply by - * @return product of prefix and scalar - * @since 2019-10-16 + * Subtracts {@code other} from this prefix and returns the result. + * + * @param other prefix to subtract + * @return difference of prefixes + * + * @since 2024-03-03 + * @since v0.5.0 */ - public UnitPrefix times(final double multiplicand) { - return valueOf(this.getMultiplier() * multiplicand); + public UnitPrefix minus(final UnitPrefix other) { + return valueOf(this.getMultiplier() - other.getMultiplier()); } /** * Adds {@code other} to this prefix and returns the result. - * + * + * @param other prefix to add + * @return sum of prefixes + * * @since 2024-03-03 + * @since v0.5.0 */ public UnitPrefix plus(final UnitPrefix other) { return valueOf(this.getMultiplier() + other.getMultiplier()); } /** - * Subtracts {@code other} from this prefix and returns the result. - * - * @since 2024-03-03 + * Multiplies this prefix by a scalar + * + * @param multiplicand number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 + * @since v0.3.0 */ - public UnitPrefix minus(final UnitPrefix other) { - return valueOf(this.getMultiplier() - other.getMultiplier()); + public UnitPrefix times(final double multiplicand) { + return valueOf(this.getMultiplier() * multiplicand); } /** * Multiplies this prefix by {@code other}. - * + * * @param other prefix to multiply by * @return product of prefixes * @since 2019-04-13 @@ -189,7 +219,7 @@ public final class UnitPrefix implements Nameable { /** * Raises this prefix to an exponent. - * + * * @param exponent exponent to raise to * @return result of exponentiation. * @since 2019-04-13 @@ -199,25 +229,23 @@ public final class UnitPrefix implements Nameable { return valueOf(Math.pow(this.getMultiplier(), exponent)); } - /** - * @return a string describing the prefix and its multiplier - */ + /** @return a string describing the prefix and its multiplier */ @Override public String toString() { if (this.getPrimaryName().isPresent()) return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(), this.multiplier); - else if (this.getSymbol().isPresent()) + if (this.getSymbol().isPresent()) return String.format("%s (\u00D7 %s)", this.getSymbol().get(), this.multiplier); - else - return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); + return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); } /** * @param ns name(s) and symbol to use * @return copy of this prefix with provided name(s) and symbol * @since 2019-11-26 + * @since v0.3.0 * @throws NullPointerException if ns is null */ public UnitPrefix withName(final NameSymbol ns) { diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java index 9a87288..b195f13 100644 --- a/src/main/java/sevenUnits/unit/UnitType.java +++ b/src/main/java/sevenUnits/unit/UnitType.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Adrien Hopkins + * Copyright (C) 2022, 2024, 2025 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 @@ -30,9 +30,15 @@ import java.util.function.Predicate; * </ul> * * @since 2022-04-10 + * @since v0.4.0 */ public enum UnitType { - METRIC, SEMI_METRIC, NON_METRIC; + /** Units that pass {@link Unit#isMetric} */ + METRIC, + /** certain exceptions like the degree Celsius */ + SEMI_METRIC, + /** Non-metric, non-excepted units */ + NON_METRIC; /** * Determines which type a unit is. The type will be: @@ -46,13 +52,13 @@ public enum UnitType { * @param isSemiMetric predicate to determine if a unit is semi-metric * @return type of unit * @since 2022-04-18 + * @since v0.4.0 */ public static final UnitType getType(Unit u, Predicate<Unit> isSemiMetric) { if (isSemiMetric.test(u)) return SEMI_METRIC; - else if (u.isMetric()) + if (u.isMetric()) return METRIC; - else - return NON_METRIC; + return NON_METRIC; } } diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java index 2d01831..e24b6e2 100644 --- a/src/main/java/sevenUnits/unit/UnitValue.java +++ b/src/main/java/sevenUnits/unit/UnitValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2021, 2022, 2024, 2025 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 @@ -17,23 +17,23 @@ package sevenUnits.unit; import java.util.Objects; -import java.util.Optional; import sevenUnits.utils.NameSymbol; /** * 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 + * @since v0.3.0 */ 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 @@ -56,67 +56,52 @@ public final class UnitValue { } /** + * @param other unit to try to convert to * @return true if this value can be converted to {@code other}. * @since 2020-10-01 + * @since v0.3.0 */ - 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) { + public boolean canConvertTo(Unit 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) { + public 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 + * Returns this unit value represented as a {@link 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. + * @return this unit as a {@link LinearUnitValue} * @since 2020-09-29 + * @since v0.3.0 */ - public final LinearUnitValue convertToBase(NameSymbol ns) { - final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + public LinearUnitValue convertToBase(NameSymbol ns) { + final var base = LinearUnit.getBase(this.unit).withName(ns); return this.convertToLinear(base); } /** + * @param newUnit unit to use for this value * @return a {@code LinearUnitValue} that is equivalent to this value. It * will have zero uncertainty. * @since 2020-09-29 + * @since v0.3.0 */ - public final LinearUnitValue convertToLinear(LinearUnit other) { - return LinearUnitValue.getExact(other, - this.getUnit().convertTo(other, this.getValue())); + public LinearUnitValue convertToLinear(LinearUnit newUnit) { + return LinearUnitValue.getExact(newUnit, + this.getUnit().convertTo(newUnit, this.getValue())); } /** @@ -128,7 +113,7 @@ public final class UnitValue { public boolean equals(Object obj) { if (!(obj instanceof UnitValue)) return false; - final UnitValue other = (UnitValue) obj; + final var other = (UnitValue) obj; return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) && Double.doubleToLongBits( this.getUnit().convertToBase(this.getValue())) == Double @@ -139,16 +124,18 @@ public final class UnitValue { /** * @return the unit * @since 2020-09-29 + * @since v0.3.0 */ - public final Unit getUnit() { + public Unit getUnit() { return this.unit; } /** * @return the value * @since 2020-09-29 + * @since v0.3.0 */ - public final double getValue() { + public double getValue() { return this.value; } @@ -160,16 +147,15 @@ public final class UnitValue { @Override public String toString() { - final Optional<String> primaryName = this.getUnit().getPrimaryName(); - final Optional<String> symbol = this.getUnit().getSymbol(); + final var primaryName = this.getUnit().getPrimaryName(); + final var symbol = this.getUnit().getSymbol(); if (primaryName.isEmpty() && symbol.isEmpty()) { - final double baseValue = this.getUnit().convertToBase(this.getValue()); + final var baseValue = this.getUnit().convertToBase(this.getValue()); return String.format("%s unnamed unit (= %s %s)", this.getValue(), baseValue, this.getUnit().getBase() .toString(unit -> unit.getSymbol().orElseThrow())); - } else { - final String unitName = symbol.orElse(primaryName.get()); - return this.getValue() + " " + unitName; } + final var unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; } } diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java deleted file mode 100644 index fef424e..0000000 --- a/src/main/java/sevenUnits/unit/Unitlike.java +++ /dev/null @@ -1,262 +0,0 @@ -/** - * 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 sevenUnits.unit; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.DoubleFunction; -import java.util.function.ToDoubleFunction; - -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.Nameable; -import sevenUnits.utils.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/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java deleted file mode 100644 index ad0d1ea..0000000 --- a/src/main/java/sevenUnits/unit/UnitlikeValue.java +++ /dev/null @@ -1,176 +0,0 @@ -/** - * 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 sevenUnits.unit; - -import java.util.Optional; - -import sevenUnits.utils.NameSymbol; - -/** - * - * @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; - } - } -} diff --git a/src/main/java/sevenUnits/unit/package-info.java b/src/main/java/sevenUnits/unit/package-info.java index 6aedb9d..c650b58 100644 --- a/src/main/java/sevenUnits/unit/package-info.java +++ b/src/main/java/sevenUnits/unit/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019-2025 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 @@ -16,7 +16,7 @@ */ /** * Everything to do with the units that make up Unit Converter. - * + * * @author Adrien Hopkins * @since 2019-10-16 * @since v0.1.0 |