summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2024-08-22 10:12:37 -0500
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2024-08-22 11:45:37 -0500
commit6f1bbc1024eae98f1815ab5f9e9cb3399f5eef9c (patch)
tree86a14f6866323d7aeaae231b91d72b3aa5bd6cb4
parent2c2b97f964327f14ce09b0b935a46aec77526560 (diff)
Allow fractional exponents
-rw-r--r--docs/roadmap.org1
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnit.java84
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnitValue.java11
-rw-r--r--src/main/java/sevenUnits/unit/UnitDatabase.java43
-rw-r--r--src/main/java/sevenUnits/utils/ObjectProduct.java87
-rw-r--r--src/main/resources/unitsfile.txt7
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