diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2024-08-22 10:12:37 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2024-08-22 11:45:37 -0500 |
commit | 6f1bbc1024eae98f1815ab5f9e9cb3399f5eef9c (patch) | |
tree | 86a14f6866323d7aeaae231b91d72b3aa5bd6cb4 | |
parent | 2c2b97f964327f14ce09b0b935a46aec77526560 (diff) |
Allow fractional exponents
-rw-r--r-- | docs/roadmap.org | 1 | ||||
-rw-r--r-- | src/main/java/sevenUnits/unit/LinearUnit.java | 84 | ||||
-rw-r--r-- | src/main/java/sevenUnits/unit/LinearUnitValue.java | 11 | ||||
-rw-r--r-- | src/main/java/sevenUnits/unit/UnitDatabase.java | 43 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/ObjectProduct.java | 87 | ||||
-rw-r--r-- | src/main/resources/unitsfile.txt | 7 |
6 files changed, 139 insertions, 94 deletions
diff --git a/docs/roadmap.org b/docs/roadmap.org index 5a3888f..bd0ccee 100644 --- a/docs/roadmap.org +++ b/docs/roadmap.org @@ -8,7 +8,6 @@ Feature Requirements: (It should not be required to handle features that aren't in 7Units; those definitions should be ignored with a warning) - 7Units's expression converter should support most or all of the conversion features supported by GNU Units: - Converting to sums of units (it should also be possible to do this in the unit converter with preset combinations) - - Non-integer exponents - (/Optional/) Inverse nonlinear conversion with the tilde prefix - (/Optional/) Nonlinear units should be specifiable in unit files. - /Any other feature not listed should be considered optional./ diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 6489229..a230f28 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -46,7 +46,7 @@ public final class LinearUnit extends Unit { Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), NameSymbol.EMPTY); } - + /** * Gets a {@code LinearUnit} from a unit and a value. For example, converts * '59 °F' to a linear unit with the value of '288.15 K' @@ -64,7 +64,7 @@ public final class LinearUnit extends Unit { Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), ns); } - + /** * @return the base unit associated with {@code unit}, as a * {@code LinearUnit}. @@ -73,7 +73,7 @@ public final class LinearUnit extends Unit { 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}. @@ -82,7 +82,7 @@ public final class LinearUnit extends Unit { 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 @@ -98,7 +98,7 @@ public final class LinearUnit extends Unit { final double conversionFactor) { return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); } - + /** * Gets a {@code LinearUnit} from a unit base and a conversion factor. In * other words, gets the product of {@code unitBase} and @@ -115,7 +115,7 @@ public final class LinearUnit extends Unit { final double conversionFactor, final NameSymbol ns) { return new LinearUnit(unitBase, conversionFactor, ns); } - + /** * The value of this unit as represented in its base form. Mathematically, * @@ -126,7 +126,7 @@ public final class LinearUnit extends Unit { * @since 2019-10-16 */ private final double conversionFactor; - + /** * Creates the {@code LinearUnit}. * @@ -139,7 +139,7 @@ public final class LinearUnit extends Unit { super(unitBase, ns); this.conversionFactor = conversionFactor; } - + /** * {@inheritDoc} * @@ -149,7 +149,7 @@ public final class LinearUnit extends Unit { protected double convertFromBase(final double value) { return value / this.getConversionFactor(); } - + /** * Converts an {@code UncertainDouble} value expressed in this unit to an * {@code UncertainValue} value expressed in {@code other}. @@ -172,9 +172,9 @@ public final class LinearUnit extends Unit { else throw new IllegalArgumentException( String.format("Cannot convert from %s to %s.", this, other)); - + } - + /** * {@inheritDoc} * @@ -184,7 +184,7 @@ public final class LinearUnit extends Unit { protected double convertToBase(final double value) { return value * this.getConversionFactor(); } - + /** * Converts an {@code UncertainDouble} to the base unit. * @@ -193,7 +193,7 @@ public final class LinearUnit extends Unit { UncertainDouble convertToBase(final UncertainDouble value) { return value.timesExact(this.getConversionFactor()); } - + /** * Divides this unit by a scalar. * @@ -205,7 +205,7 @@ public final class LinearUnit extends Unit { public LinearUnit dividedBy(final double divisor) { return valueOf(this.getBase(), this.getConversionFactor() / divisor); } - + /** * Returns the quotient of this unit and another. * @@ -217,14 +217,14 @@ public final class LinearUnit extends Unit { */ public LinearUnit dividedBy(final LinearUnit divisor) { Objects.requireNonNull(divisor, "other must not be null"); - + // divide the units final ObjectProduct<BaseUnit> base = this.getBase() .dividedBy(divisor.getBase()); return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); } - + /** * {@inheritDoc} * @@ -239,7 +239,7 @@ public final class LinearUnit extends Unit { && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); } - + /** * @return conversion factor * @since 2019-10-16 @@ -247,7 +247,7 @@ public final class LinearUnit extends Unit { public double getConversionFactor() { return this.conversionFactor; } - + /** * {@inheritDoc} * @@ -258,7 +258,7 @@ public final class LinearUnit extends Unit { return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); } - + /** * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there * is a {@code BaseUnit b} where @@ -268,7 +268,7 @@ public final class LinearUnit extends Unit { public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); } - + /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 @@ -276,7 +276,7 @@ public final class LinearUnit extends Unit { public boolean isCoherent() { return this.getConversionFactor() == 1; } - + /** * Returns the difference of this unit and another. * <p> @@ -296,18 +296,18 @@ public final class LinearUnit extends Unit { */ public LinearUnit minus(final LinearUnit subtrahend) { Objects.requireNonNull(subtrahend, "addend must not be null."); - + // reject subtrahends that cannot be added to this unit if (!this.getBase().equals(subtrahend.getBase())) throw new IllegalArgumentException(String.format( "Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahend)); - + // subtract the units return valueOf(this.getBase(), this.getConversionFactor() - subtrahend.getConversionFactor()); } - + /** * Returns the sum of this unit and another. * <p> @@ -327,18 +327,18 @@ public final class LinearUnit extends Unit { */ public LinearUnit plus(final LinearUnit addend) { Objects.requireNonNull(addend, "addend must not be null."); - + // reject addends that cannot be added to this unit if (!this.getBase().equals(addend.getBase())) throw new IllegalArgumentException(String.format( "Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - + // add the units return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); } - + /** * Multiplies this unit by a scalar. * @@ -350,7 +350,7 @@ public final class LinearUnit extends Unit { public LinearUnit times(final double multiplier) { return valueOf(this.getBase(), this.getConversionFactor() * multiplier); } - + /** * Returns the product of this unit and another. * @@ -362,21 +362,21 @@ public final class LinearUnit extends Unit { */ public LinearUnit times(final LinearUnit multiplier) { Objects.requireNonNull(multiplier, "other must not be null"); - + // multiply the units final ObjectProduct<BaseUnit> base = this.getBase() .times(multiplier.getBase()); return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); } - + @Override public String toDefinitionString() { return Double.toString(this.conversionFactor) + (this.getBase().equals(ObjectProduct.empty()) ? "" : " " + this.getBase().toString(BaseUnit::getShortName)); } - + /** * Returns this unit but to an exponent. * @@ -389,12 +389,24 @@ public final class LinearUnit extends Unit { return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); } - + + /** + * Returns this unit to an exponent, rounding the resulting dimensions to the + * nearest integer. + * + * @since 2024-08-22 + * @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); } - + /** * Returns the result of applying {@code prefix} to this unit. * <p> @@ -413,7 +425,7 @@ public final class LinearUnit extends Unit { */ public LinearUnit withPrefix(final UnitPrefix prefix) { final LinearUnit unit = this.times(prefix.getMultiplier()); - + // create new name and symbol, if possible final String name; if (this.getPrimaryName().isPresent() @@ -422,14 +434,14 @@ public final class LinearUnit extends Unit { } else { name = null; } - + final String symbol; if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { symbol = prefix.getSymbol().get() + this.getSymbol().get(); } else { symbol = null; } - + return unit.withName(NameSymbol.ofNullable(name, symbol)); } } diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index fad3eb0..db2936c 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -338,6 +338,17 @@ public final class LinearUnitValue { this.value.toExponentExact(exponent)); } + /** + * Raises this value to an exponent, rounding all dimensions to integers. + * + * @since 2024-08-22 + * @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); diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 05c31c4..514b27d 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -44,7 +44,6 @@ 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; @@ -1117,20 +1116,13 @@ public final class UnitDatabase { */ private static final 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 double exponent = exponentUnit.getConversionFactor(); + return base.toExponentRounded(exponent); } /** @@ -1143,20 +1135,13 @@ public final class UnitDatabase { */ private static final 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 double exponent = exponentValue.getValueExact(); + return base.toExponentRounded(exponent); } /** diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 5a29d79..4ed70be 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -35,6 +35,12 @@ import java.util.function.Function; */ public class ObjectProduct<T> implements Nameable { /** + * If {@link #toExponentRounded}'s rounding changes exponents by more than + * this value, a warning will be printed to standard error. + */ + private static final double ROUND_WARN_THRESHOLD = 1e-12; + + /** * Returns an empty ObjectProduct of a certain type * * @param <T> type of objects that can be multiplied @@ -44,7 +50,7 @@ public class ObjectProduct<T> implements Nameable { public static final <T> ObjectProduct<T> empty() { return new ObjectProduct<>(new HashMap<>()); } - + /** * Gets an {@code ObjectProduct} from an object-to-integer mapping * @@ -57,7 +63,7 @@ public class ObjectProduct<T> implements Nameable { final Map<T, Integer> map) { return new ObjectProduct<>(new HashMap<>(map)); } - + /** * Gets an ObjectProduct that has one of the inputted argument, and nothing * else. @@ -73,7 +79,7 @@ public class ObjectProduct<T> implements Nameable { map.put(object, 1); return new ObjectProduct<>(map); } - + /** * The objects that make up the product, mapped to their exponents. This map * treats zero as null, and is immutable. @@ -81,12 +87,12 @@ public class ObjectProduct<T> implements Nameable { * @since 2019-10-16 */ final Map<T, Integer> exponents; - + /** * The object's name and symbol */ private final NameSymbol nameSymbol; - + /** * Creates a {@code ObjectProduct} without a name/symbol. * @@ -96,7 +102,7 @@ public class ObjectProduct<T> implements Nameable { ObjectProduct(final Map<T, Integer> exponents) { this(exponents, NameSymbol.EMPTY); } - + /** * Creates the {@code ObjectProduct}. * @@ -110,7 +116,7 @@ public class ObjectProduct<T> implements Nameable { e -> !Integer.valueOf(0).equals(e.getValue()))); this.nameSymbol = nameSymbol; } - + /** * Calculates the quotient of two products * @@ -125,17 +131,17 @@ public class ObjectProduct<T> implements Nameable { final Set<T> objects = new HashSet<>(); objects.addAll(this.getBaseSet()); objects.addAll(other.getBaseSet()); - + // get a list of all exponents final Map<T, Integer> map = new HashMap<>(objects.size()); for (final T key : objects) { map.put(key, this.getExponent(key) - other.getExponent(key)); } - + // create the product return new ObjectProduct<>(map); } - + // this method relies on the use of ZeroIsNullMap @Override public boolean equals(final Object obj) { @@ -146,7 +152,7 @@ public class ObjectProduct<T> implements Nameable { final ObjectProduct<?> other = (ObjectProduct<?>) obj; return Objects.equals(this.exponents, other.exponents); } - + /** * @return immutable map mapping objects to exponents * @since 2019-10-16 @@ -154,7 +160,7 @@ public class ObjectProduct<T> implements Nameable { public Map<T, Integer> exponentMap() { return this.exponents; } - + /** * @return a set of all of the base objects with non-zero exponents that make * up this dimension. @@ -163,7 +169,7 @@ public class ObjectProduct<T> implements Nameable { */ public final Set<T> getBaseSet() { final Set<T> dimensions = new HashSet<>(); - + // add all dimensions with a nonzero exponent - zero exponents shouldn't // be there in the first place for (final T dimension : this.exponents.keySet()) { @@ -171,10 +177,10 @@ public class ObjectProduct<T> implements Nameable { dimensions.add(dimension); } } - + return dimensions; } - + /** * Gets the exponent for a specific dimension. * @@ -186,17 +192,17 @@ public class ObjectProduct<T> implements Nameable { public int getExponent(final T dimension) { return this.exponents.getOrDefault(dimension, 0); } - + @Override public NameSymbol getNameSymbol() { return this.nameSymbol; } - + @Override public int hashCode() { return Objects.hash(this.exponents); } - + /** * @return true if this product is a single object, i.e. it has one exponent * of one and no other nonzero exponents @@ -214,7 +220,7 @@ public class ObjectProduct<T> implements Nameable { } return oneCount == 1 && !twoOrMore; } - + /** * Multiplies this product by another * @@ -229,17 +235,17 @@ public class ObjectProduct<T> implements Nameable { final Set<T> objects = new HashSet<>(); objects.addAll(this.getBaseSet()); objects.addAll(other.getBaseSet()); - + // get a list of all exponents final Map<T, Integer> map = new HashMap<>(objects.size()); for (final T key : objects) { map.put(key, this.getExponent(key) + other.getExponent(key)); } - + // create the product return new ObjectProduct<>(map); } - + /** * Returns this product, but to an exponent * @@ -254,7 +260,32 @@ public class ObjectProduct<T> implements Nameable { } return new ObjectProduct<>(map); } - + + /** + * Returns this product to an exponent, where every dimension is rounded to + * the nearest integer. + * + * This function will send a warning (via {@link System.err}) if the rounding + * significantly changes the value. + * + * @since 2024-08-22 + */ + public ObjectProduct<T> toExponentRounded(final double exponent) { + final Map<T, Integer> map = new HashMap<>(this.exponents); + for (final T key : this.exponents.keySet()) { + final double newExponent = this.getExponent(key) * exponent; + if (Math.abs( + newExponent - Math.round(newExponent)) > ROUND_WARN_THRESHOLD) { + System.err.printf( + "Exponent Rounding Warning: Dimension exponents must be integers, so %d ^ %g = %g was rounded to %d.\n", + this.getExponent(key), exponent, newExponent, + Math.round(newExponent)); + } + map.put(key, (int) Math.round(newExponent)); + } + return new ObjectProduct<>(map); + } + /** * Converts this product to a string using the objects' * {@link Object#toString()} method (or {@link Nameable#getShortName} if @@ -271,7 +302,7 @@ public class ObjectProduct<T> implements Nameable { .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName() : o.toString()); } - + /** * Converts this product to a string. The objects that make up this product * are represented by {@code objectToString} @@ -283,7 +314,7 @@ public class ObjectProduct<T> implements Nameable { public String toString(final Function<T, String> objectToString) { final List<String> positiveStringComponents = new ArrayList<>(); final List<String> negativeStringComponents = new ArrayList<>(); - + // for each base object that makes up this object, add it and its exponent for (final T object : this.getBaseSet()) { final int exponent = this.exponents.get(object); @@ -297,15 +328,15 @@ public class ObjectProduct<T> implements Nameable { objectToString.apply(object), -exponent)); } } - + final String positiveString = positiveStringComponents.isEmpty() ? "1" : String.join(" * ", positiveStringComponents); final String negativeString = negativeStringComponents.isEmpty() ? "" : " / " + String.join(" * ", negativeStringComponents); - + return positiveString + negativeString; } - + /** * @return named version of this {@code ObjectProduct}, using data from * {@code nameSymbol} diff --git a/src/main/resources/unitsfile.txt b/src/main/resources/unitsfile.txt index ecd9bb6..dc33abd 100644 --- a/src/main/resources/unitsfile.txt +++ b/src/main/resources/unitsfile.txt @@ -165,6 +165,7 @@ gregorianyear 365.2425 day gregorianmonth gregorianyear / 12 # Other non-SI "metric" units +are 100 m^2 litre 0.001 m^3 liter litre l litre @@ -188,11 +189,17 @@ inch foot / 12 in inch yard 3 foot yd yard +chain 66 ft +ch chain +furlong 10 chain mile 1760 yard mi mile ftin foot; inch ydftin yard; foot; inch +# Imperial area units +acre chain * furlong + # Compressed notation kph km / hour mph mile / hour |