From b495b7029ebdcd56a41ea09811343a55c432b0a0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 26 Jul 2020 14:43:11 -0500 Subject: Added UnitValue and LinearUnitValue --- src/org/unitConverter/unit/LinearUnitValue.java | 300 ++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 src/org/unitConverter/unit/LinearUnitValue.java (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java new file mode 100644 index 0000000..8daabf7 --- /dev/null +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -0,0 +1,300 @@ +/** + * + */ +package org.unitConverter.unit; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.Optional; + +/** + * A possibly uncertain value expressed in a linear unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class LinearUnitValue { + private final LinearUnit unit; + private final double value; + private final double uncertainty; + + /** + * Gets an exact {@code UnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code UnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(LinearUnit unit, double value) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); + } + + /** + * Gets an uncertain {@code UnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code UnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(LinearUnit unit, double value, double uncertainty) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); + } + + /** + * @param unit unit to express as + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @since 2020-07-26 + */ + private LinearUnitValue(LinearUnit unit, double value, double uncertainty) { + this.unit = unit; + this.value = value; + this.uncertainty = uncertainty; + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(LinearUnit other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, value), this.unit.convertTo(other, uncertainty)); + } + + /** + * 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 + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits(other.unit.convertToBase(other.getValue())) + && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double + .doubleToLongBits(other.getRelativeUncertainty()); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(LinearUnitValue other) { + if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + double thisBaseValue = this.unit.convertToBase(this.value); + double otherBaseValue = other.unit.convertToBase(other.value); + double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); + double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); + return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); + } + + /** + * @return the unit + * + * @since 2020-07-26 + */ + public final LinearUnit getUnit() { + return unit; + } + + /** + * @return the value + * + * @since 2020-07-26 + */ + public final double getValue() { + return value; + } + + /** + * @return absolute uncertainty of value + * + * @since 2020-07-26 + */ + public final double getUncertainty() { + return uncertainty; + } + + /** + * @return relative uncertainty of value + * + * @since 2020-07-26 + */ + public final double getRelativeUncertainty() { + return uncertainty / value; + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), this.getRelativeUncertainty()); + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-07-26 + */ + public final boolean isExact() { + return uncertainty == 0; + } + + /** + * Returns the sum of this value and another, expressed in this value's unit + * + * @param addend + * value to add + * @return sum of values + * @throws IllegalArgumentException + * if {@code addend} has a unit that is not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue plus(LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value + otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Returns the difference of this value and another, expressed in this value's unit + * + * @param subtrahend + * value to subtract + * @return difference of values + * @throws IllegalArgumentException + * if {@code subtrahend} has a unit that is not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value - otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + @Override + public String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of single + * numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(boolean showUncertainty) { + Optional primaryName = this.unit.getPrimaryName(); + Optional symbol = this.unit.getSymbol(); + String chosenName = symbol.orElse(primaryName.orElse(null)); + + final double baseValue = this.unit.convertToBase(this.value); + final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + + // get rounded strings + final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; + if (this.isExact()) { + valueString = Double.toString(value); + baseValueString = Double.toString(baseValue); + uncertaintyString = "0"; + baseUncertaintyString = "0"; + } else { + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the uncertainty. + BigDecimal roundedUncertainty = bigUncertainty + .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedUncertainty.unscaledValue().intValue() >= 20) { + roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + + if (primaryName.isEmpty() && symbol.isEmpty()) { + final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); + final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); + + BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { + roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); + } + final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), + RoundingMode.HALF_EVEN); + + baseValueString = roundedBaseValue.toString(); + baseUncertaintyString = roundedBaseUncertainty.toString(); + } else { + // unused + baseValueString = ""; + baseUncertaintyString = ""; + } + } + + // create string + if (showUncertainty) { + if (primaryName.isEmpty() && symbol.isEmpty()) { + return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, + baseValueString, baseUncertaintyString, this.unit.getBase()); + } else { + return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); + } + } else { + if (primaryName.isEmpty() && symbol.isEmpty()) { + return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); + } else { + return String.format("%s %s", valueString, chosenName); + } + } + } +} -- cgit v1.2.3 From 7549ac295f16b6de31abdfd832225b8b1824aaa8 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 28 Jul 2020 10:17:27 -0500 Subject: Added times, dividedBy and toExponent to LinearUnitValue --- .../converterGUI/UnitConverterGUI.java | 4 +- src/org/unitConverter/unit/LinearUnitValue.java | 601 ++++++++++++--------- 2 files changed, 338 insertions(+), 267 deletions(-) (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index daae2c6..986cb52 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -26,10 +26,8 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.text.DecimalFormat; -import java.text.FieldPosition; import java.text.NumberFormat; import java.text.ParseException; -import java.text.ParsePosition; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -559,7 +557,7 @@ final class UnitConverterGUI { if (value instanceof Double) { return (double) value; } else if (value instanceof Long) { - return (double) (((Long) value).longValue()); + return (((Long) value).longValue()); } else throw new AssertionError(); } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 8daabf7..f16b19f 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -18,283 +18,356 @@ import java.util.Optional; * @since 2020-07-26 */ public final class LinearUnitValue { - private final LinearUnit unit; - private final double value; - private final double uncertainty; - - /** - * Gets an exact {@code UnitValue} - * - * @param unit unit to express with - * @param value value to express - * @return exact {@code UnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue getExact(LinearUnit unit, double value) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); - } + /** + * Gets an exact {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); + } - /** - * Gets an uncertain {@code UnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @return uncertain {@code UnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue of(LinearUnit unit, double value, double uncertainty) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); - } + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, final double value, final double uncertainty) { + return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); + } - /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @since 2020-07-26 - */ - private LinearUnitValue(LinearUnit unit, double value, double uncertainty) { - this.unit = unit; - this.value = value; - this.uncertainty = uncertainty; - } + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param relativeUncertainty relative uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-28 + */ + public static final LinearUnitValue ofRelative(final LinearUnit unit, final double value, + final double relativeUncertainty) { + return LinearUnitValue.of(unit, value, relativeUncertainty * value); + } - /** - * @param other a {@code LinearUnit} - * @return true iff this value can be represented with {@code other}. - * @since 2020-07-26 - */ - public final boolean canConvertTo(LinearUnit other) { - return this.unit.canConvertTo(other); - } + private final LinearUnit unit; + private final double value; + private final double uncertainty; - /** - * Returns a LinearUnitValue that represents the same value expressed in a - * different unit - * - * @param other new unit to express value in - * @return value expressed in {@code other} - * @since 2020-07-26 - */ - public final LinearUnitValue convertTo(LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, value), this.unit.convertTo(other, uncertainty)); - } + /** + * @param unit unit to express as + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final double value, final double uncertainty) { + this.unit = unit; + this.value = value; + this.uncertainty = uncertainty; + } - /** - * 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 - */ - @Override - public boolean equals(Object obj) { - if (!(obj instanceof LinearUnitValue)) - return false; - LinearUnitValue other = (LinearUnitValue) obj; - return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits(other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); - } - - /** - * @param other another {@code LinearUnitValue} - * @return true iff this and other are within each other's uncertainty range - * - * @since 2020-07-26 - */ - public boolean equivalent(LinearUnitValue other) { - if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) - return false; - double thisBaseValue = this.unit.convertToBase(this.value); - double otherBaseValue = other.unit.convertToBase(other.value); - double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); - double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); - } + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } - /** - * @return the unit - * - * @since 2020-07-26 - */ - public final LinearUnit getUnit() { - return unit; - } + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), + this.unit.convertTo(other, this.uncertainty)); + } - /** - * @return the value - * - * @since 2020-07-26 - */ - public final double getValue() { - return value; - } + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value / divisor, this.uncertainty / divisor); + } - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return uncertainty; - } + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), this.value / divisor.value, + Math.hypot(this.getRelativeUncertainty(), divisor.getRelativeUncertainty())); + } - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return uncertainty / value; - } - - @Override - public int hashCode() { - return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), this.getRelativeUncertainty()); - } + /** + * 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 + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits(other.unit.convertToBase(other.getValue())) + && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double + .doubleToLongBits(other.getRelativeUncertainty()); + } - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return uncertainty == 0; - } - - /** - * Returns the sum of this value and another, expressed in this value's unit - * - * @param addend - * value to add - * @return sum of values - * @throws IllegalArgumentException - * if {@code addend} has a unit that is not compatible for addition - * @since 2020-07-26 - */ - public LinearUnitValue plus(LinearUnitValue addend) { - Objects.requireNonNull(addend, "addend may not be null"); - - if (!this.canConvertTo(addend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - - final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } - - /** - * Returns the difference of this value and another, expressed in this value's unit - * - * @param subtrahend - * value to subtract - * @return difference of values - * @throws IllegalArgumentException - * if {@code subtrahend} has a unit that is not compatible for addition - * @since 2020-07-26 - */ - public LinearUnitValue minus(LinearUnitValue subtrahend) { - Objects.requireNonNull(subtrahend, "subtrahend may not be null"); - - if (!this.canConvertTo(subtrahend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - - final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final double thisBaseValue = this.unit.convertToBase(this.value); + final double otherBaseValue = other.unit.convertToBase(other.value); + final double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); + final double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); + return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); + } - @Override - public String toString() { - return this.toString(!this.isExact()); - } + /** + * @return relative uncertainty of value + * + * @since 2020-07-26 + */ + public final double getRelativeUncertainty() { + return this.uncertainty / this.value; + } - /** - * Returns a string representing the object.
- * If the attached unit has a name or symbol, the string looks like "12 km". - * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". - *

- * If showUncertainty is true, strings like "35 ± 8" are shown instead of single - * numbers. - *

- * Non-exact values are rounded intelligently based on their uncertainty. - * - * @since 2020-07-26 - */ - public String toString(boolean showUncertainty) { - Optional primaryName = this.unit.getPrimaryName(); - Optional symbol = this.unit.getSymbol(); - String chosenName = symbol.orElse(primaryName.orElse(null)); - - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); - - // get rounded strings - final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the uncertainty. - BigDecimal roundedUncertainty = bigUncertainty - .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), - RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } - } + /** + * @return absolute uncertainty of value + * + * @since 2020-07-26 + */ + public final double getUncertainty() { + return this.uncertainty; + } + + /** + * @return the unit + * + * @since 2020-07-26 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * + * @since 2020-07-26 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), + this.getRelativeUncertainty()); + } - // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) { - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, - baseValueString, baseUncertaintyString, this.unit.getBase()); - } else { - return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); - } - } else { - if (primaryName.isEmpty() && symbol.isEmpty()) { - return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); - } else { - return String.format("%s %s", valueString, chosenName); - } + /** + * @return true iff the value has no uncertainty + * + * @since 2020-07-26 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of this value and another, expressed in this value's + * unit + * + * @param subtrahend value to subtract + * @return difference of values + * @throws IllegalArgumentException if {@code subtrahend} has a unit that is not + * compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value - otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Returns the sum of this value and another, expressed in this value's unit + * + * @param addend value to add + * @return sum of values + * @throws IllegalArgumentException if {@code addend} has a unit that is not + * compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException( + String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value + otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value * multiplier, this.uncertainty * multiplier); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), this.value * multiplier.value, + Math.hypot(this.getRelativeUncertainty(), multiplier.getRelativeUncertainty())); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), Math.pow(this.value, exponent), + this.getRelativeUncertainty() * Math.sqrt(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of single + * numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional primaryName = this.unit.getPrimaryName(); + final Optional symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final double baseValue = this.unit.convertToBase(this.value); + final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + + // get rounded strings + final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; + if (this.isExact()) { + valueString = Double.toString(this.value); + baseValueString = Double.toString(baseValue); + uncertaintyString = "0"; + baseUncertaintyString = "0"; + } else { + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the uncertainty. + BigDecimal roundedUncertainty = bigUncertainty + .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedUncertainty.unscaledValue().intValue() >= 20) { + roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + + if (primaryName.isEmpty() && symbol.isEmpty()) { + final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); + final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); + + BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); + if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { + roundedBaseUncertainty = bigBaseUncertainty.setScale( + bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); } + final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), + RoundingMode.HALF_EVEN); + + baseValueString = roundedBaseValue.toString(); + baseUncertaintyString = roundedBaseUncertainty.toString(); + } else { + // unused + baseValueString = ""; + baseUncertaintyString = ""; + } + } + + // create string + if (showUncertainty) { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, + baseValueString, baseUncertaintyString, this.unit.getBase()); + else + return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); + } else { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); } + } } -- cgit v1.2.3 From cf76cf66ea2039cd3e3052c940784dd88a87e2bd Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 28 Jul 2020 10:51:19 -0500 Subject: Added some tests for UnitValue and LinearUnitValue. --- src/org/unitConverter/unit/LinearUnitValue.java | 763 +++++++++++++----------- src/org/unitConverter/unit/UnitTest.java | 77 ++- 2 files changed, 467 insertions(+), 373 deletions(-) (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index f16b19f..74b0400 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -8,6 +8,8 @@ import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; +import org.unitConverter.math.DecimalComparison; + /** * A possibly uncertain value expressed in a linear unit. * @@ -18,356 +20,417 @@ import java.util.Optional; * @since 2020-07-26 */ public final class LinearUnitValue { - /** - * Gets an exact {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @return exact {@code LinearUnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, 0); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-26 - */ - public static final LinearUnitValue of(final LinearUnit unit, final double value, final double uncertainty) { - return new LinearUnitValue(Objects.requireNonNull(unit, "unit must not be null"), value, uncertainty); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param relativeUncertainty relative uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-28 - */ - public static final LinearUnitValue ofRelative(final LinearUnit unit, final double value, - final double relativeUncertainty) { - return LinearUnitValue.of(unit, value, relativeUncertainty * value); - } - - private final LinearUnit unit; - private final double value; - private final double uncertainty; - - /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value - * @since 2020-07-26 - */ - private LinearUnitValue(final LinearUnit unit, final double value, final double uncertainty) { - this.unit = unit; - this.value = value; - this.uncertainty = uncertainty; - } - - /** - * @param other a {@code LinearUnit} - * @return true iff this value can be represented with {@code other}. - * @since 2020-07-26 - */ - public final boolean canConvertTo(final LinearUnit other) { - return this.unit.canConvertTo(other); - } - - /** - * Returns a LinearUnitValue that represents the same value expressed in a - * different unit - * - * @param other new unit to express value in - * @return value expressed in {@code other} - * @since 2020-07-26 - */ - public final LinearUnitValue convertTo(final LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), - this.unit.convertTo(other, this.uncertainty)); - } - - /** - * Divides this value by a scalar - * - * @param divisor value to divide by - * @return multiplied value - * @since 2020-07-28 - */ - public LinearUnitValue dividedBy(final double divisor) { - return LinearUnitValue.of(this.unit, this.value / divisor, this.uncertainty / divisor); - } - - /** - * Divides this value by another value - * - * @param divisor value to multiply by - * @return quotient - * @since 2020-07-28 - */ - public LinearUnitValue dividedBy(final LinearUnitValue divisor) { - return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), this.value / divisor.value, - Math.hypot(this.getRelativeUncertainty(), divisor.getRelativeUncertainty())); - } - - /** - * 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 - */ - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof LinearUnitValue)) - return false; - final LinearUnitValue other = (LinearUnitValue) obj; - return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits(other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); - } - - /** - * @param other another {@code LinearUnitValue} - * @return true iff this and other are within each other's uncertainty range - * - * @since 2020-07-26 - */ - public boolean equivalent(final LinearUnitValue other) { - if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) - return false; - final double thisBaseValue = this.unit.convertToBase(this.value); - final double otherBaseValue = other.unit.convertToBase(other.value); - final double thisBaseUncertainty = this.unit.convertToBase(this.uncertainty); - final double otherBaseUncertainty = other.unit.convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math.min(thisBaseUncertainty, otherBaseUncertainty); - } - - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return this.uncertainty / this.value; - } - - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return this.uncertainty; - } - - /** - * @return the unit - * - * @since 2020-07-26 - */ - public final LinearUnit getUnit() { - return this.unit; - } - - /** - * @return the value - * - * @since 2020-07-26 - */ - public final double getValue() { - return this.value; - } - - @Override - public int hashCode() { - return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue()), - this.getRelativeUncertainty()); - } - - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return this.uncertainty == 0; - } - - /** - * Returns the difference of this value and another, expressed in this value's - * unit - * - * @param subtrahend value to subtract - * @return difference of values - * @throws IllegalArgumentException if {@code subtrahend} has a unit that is not - * compatible for addition - * @since 2020-07-26 - */ - public LinearUnitValue minus(final LinearUnitValue subtrahend) { - Objects.requireNonNull(subtrahend, "subtrahend may not be null"); - - if (!this.canConvertTo(subtrahend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - - final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } - - /** - * Returns the sum of this value and another, expressed in this value's unit - * - * @param addend value to add - * @return sum of values - * @throws IllegalArgumentException if {@code addend} has a unit that is not - * compatible for addition - * @since 2020-07-26 - */ - public LinearUnitValue plus(final LinearUnitValue addend) { - Objects.requireNonNull(addend, "addend may not be null"); - - if (!this.canConvertTo(addend.unit)) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - - final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); - } - - /** - * Multiplies this value by a scalar - * - * @param multiplier value to multiply by - * @return multiplied value - * @since 2020-07-28 - */ - public LinearUnitValue times(final double multiplier) { - return LinearUnitValue.of(this.unit, this.value * multiplier, this.uncertainty * multiplier); - } - - /** - * Multiplies this value by another value - * - * @param multiplier value to multiply by - * @return product - * @since 2020-07-28 - */ - public LinearUnitValue times(final LinearUnitValue multiplier) { - return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), this.value * multiplier.value, - Math.hypot(this.getRelativeUncertainty(), multiplier.getRelativeUncertainty())); - } - - /** - * Raises a value to an exponent - * - * @param exponent exponent to raise to - * @return result of exponentiation - * @since 2020-07-28 - */ - public LinearUnitValue toExponent(final int exponent) { - return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), Math.pow(this.value, exponent), - this.getRelativeUncertainty() * Math.sqrt(exponent)); - } - - @Override - public String toString() { - return this.toString(!this.isExact()); - } - - /** - * Returns a string representing the object.
- * If the attached unit has a name or symbol, the string looks like "12 km". - * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". - *

- * If showUncertainty is true, strings like "35 ± 8" are shown instead of single - * numbers. - *

- * Non-exact values are rounded intelligently based on their uncertainty. - * - * @since 2020-07-26 - */ - public String toString(final boolean showUncertainty) { - final Optional primaryName = this.unit.getPrimaryName(); - final Optional symbol = this.unit.getSymbol(); - final String chosenName = symbol.orElse(primaryName.orElse(null)); - - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); - - // get rounded strings - final String valueString, baseValueString, uncertaintyString, baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(this.value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the uncertainty. - BigDecimal roundedUncertainty = bigUncertainty - .setScale(bigUncertainty.scale() - bigUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale(bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue.setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal.valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 2, RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty.setScale( - bigBaseUncertainty.scale() - bigBaseUncertainty.precision() + 1, RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale(roundedBaseUncertainty.scale(), - RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } + /** + * Gets an exact {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, + final double value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value, 0); } - - // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", valueString, uncertaintyString, - baseValueString, baseUncertaintyString, this.unit.getBase()); - else - return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); - } else { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); - else - return String.format("%s %s", valueString, chosenName); + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, + final double value, final double uncertainty) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value, + uncertainty); + } + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param relativeUncertainty relative uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-28 + */ + public static final LinearUnitValue ofRelative(final LinearUnit unit, + final double value, final double relativeUncertainty) { + return LinearUnitValue.of(unit, value, relativeUncertainty * value); + } + + private final LinearUnit unit; + private final double value; + private final double uncertainty; + + /** + * @param unit unit to express as + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final double value, + final double uncertainty) { + this.unit = unit; + this.value = value; + this.uncertainty = uncertainty; + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), + this.unit.convertTo(other, this.uncertainty)); + } + + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value / divisor, + this.uncertainty / divisor); + } + + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), + this.value / divisor.value, + Math.hypot(this.getRelativeUncertainty(), + divisor.getRelativeUncertainty())); + } + + /** + * 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 + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits( + this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.unit.convertToBase(other.getValue())) + && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double + .doubleToLongBits(other.getRelativeUncertainty()); + } + + /** + * 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. + *

+ * If avoidFPErrors is true, this method will attempt to avoid floating-point + * errors, at the cost of not always being transitive. + * + * @since 2020-07-28 + */ + public boolean equals(final Object obj, final boolean avoidFPErrors) { + if (!avoidFPErrors) + return this.equals(obj); + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && DecimalComparison.equals(this.unit.convertToBase(this.value), + other.unit.convertToBase(other.value)) + && DecimalComparison.equals(this.getRelativeUncertainty(), + other.getRelativeUncertainty()); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null + || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final double thisBaseValue = this.unit.convertToBase(this.value); + final double otherBaseValue = other.unit.convertToBase(other.value); + final double thisBaseUncertainty = this.unit + .convertToBase(this.uncertainty); + final double otherBaseUncertainty = other.unit + .convertToBase(other.uncertainty); + return Math.abs(thisBaseValue - otherBaseValue) <= Math + .min(thisBaseUncertainty, otherBaseUncertainty); + } + + /** + * @return relative uncertainty of value + * + * @since 2020-07-26 + */ + public final double getRelativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * @return absolute uncertainty of value + * + * @since 2020-07-26 + */ + public final double getUncertainty() { + return this.uncertainty; + } + + /** + * @return the unit + * + * @since 2020-07-26 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * + * @since 2020-07-26 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), + this.unit.convertToBase(this.getValue()), + this.getRelativeUncertainty()); + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-07-26 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of this value and another, expressed in this + * value's unit + * + * @param subtrahend value to subtract + * @return difference of values + * @throws IllegalArgumentException if {@code subtrahend} has a unit that is + * not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", + this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value - otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Returns the sum of this value and another, expressed in this value's unit + * + * @param addend value to add + * @return sum of values + * @throws IllegalArgumentException if {@code addend} has a unit that is not + * compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this.unit, + addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, this.value + otherConverted.value, + Math.hypot(this.uncertainty, otherConverted.uncertainty)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value * multiplier, + this.uncertainty * multiplier); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), + this.value * multiplier.value, + Math.hypot(this.getRelativeUncertainty(), + multiplier.getRelativeUncertainty())); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), + Math.pow(this.value, exponent), + this.getRelativeUncertainty() * Math.sqrt(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of + * single numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional primaryName = this.unit.getPrimaryName(); + final Optional symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final double baseValue = this.unit.convertToBase(this.value); + final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + + // get rounded strings + final String valueString, baseValueString, uncertaintyString, + baseUncertaintyString; + if (this.isExact()) { + valueString = Double.toString(this.value); + baseValueString = Double.toString(baseValue); + uncertaintyString = "0"; + baseUncertaintyString = "0"; + } else { + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the + // uncertainty. + BigDecimal roundedUncertainty = bigUncertainty.setScale( + bigUncertainty.scale() - bigUncertainty.precision() + 2, + RoundingMode.HALF_EVEN); + if (roundedUncertainty.unscaledValue().intValue() >= 20) { + roundedUncertainty = bigUncertainty.setScale( + bigUncertainty.scale() - bigUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedValue = bigValue + .setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + + if (primaryName.isEmpty() && symbol.isEmpty()) { + final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); + final BigDecimal bigBaseUncertainty = BigDecimal + .valueOf(baseUncertainty); + + BigDecimal roundedBaseUncertainty = bigBaseUncertainty + .setScale( + bigBaseUncertainty.scale() + - bigBaseUncertainty.precision() + 2, + RoundingMode.HALF_EVEN); + if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { + roundedBaseUncertainty = bigBaseUncertainty + .setScale( + bigBaseUncertainty.scale() + - bigBaseUncertainty.precision() + 1, + RoundingMode.HALF_EVEN); + } + final BigDecimal roundedBaseValue = bigBaseValue.setScale( + roundedBaseUncertainty.scale(), RoundingMode.HALF_EVEN); + + baseValueString = roundedBaseValue.toString(); + baseUncertaintyString = roundedBaseUncertainty.toString(); + } else { + // unused + baseValueString = ""; + baseUncertaintyString = ""; + } + } + + // create string + if (showUncertainty) { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", + valueString, uncertaintyString, baseValueString, + baseUncertaintyString, this.unit.getBase()); + else + return String.format("(%s ± %s) %s", valueString, uncertaintyString, + chosenName); + } else { + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); + } } - } } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index c078cfc..2cf3126 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -17,6 +17,8 @@ package org.unitConverter.unit; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -25,7 +27,8 @@ import org.junit.jupiter.api.Test; import org.unitConverter.math.DecimalComparison; /** - * Testing the various Unit classes. This is NOT part of this program's public API. + * Testing the various Unit classes. This is NOT part of this program's public + * API. * * @author Adrien Hopkins * @since 2018-12-22 @@ -34,69 +37,97 @@ import org.unitConverter.math.DecimalComparison; class UnitTest { /** A random number generator */ private static final Random rng = ThreadLocalRandom.current(); - + @Test public void testAdditionAndSubtraction() { - final LinearUnit inch = SI.METRE.times(0.0254); - final LinearUnit foot = SI.METRE.times(0.3048); - + final LinearUnit inch = SI.METRE.times(0.0254) + .withName(NameSymbol.of("inch", "in")); + final LinearUnit foot = SI.METRE.times(0.3048) + .withName(NameSymbol.of("foot", "ft")); + assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); + + // test with LinearUnitValue + final LinearUnitValue value1 = LinearUnitValue.getExact(SI.METRE, 15); + final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120); + final LinearUnitValue value3 = LinearUnitValue.getExact(SI.METRE, 0.5); + final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); + + // make sure addition is done correctly + assertEquals(51.576, value1.plus(value2).getValue(), 0.001); + assertEquals(15.5, value1.plus(value3).getValue()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValue(), 0.001); + + // make sure addition uses the correct unit, and is still associative + // (ignoring floating-point rounding errors) + assertEquals(SI.METRE, value1.plus(value2).getUnit()); + assertEquals(SI.METRE, value1.plus(value2).plus(value3).getUnit()); + assertEquals(foot, value2.plus(value1).getUnit()); + assertTrue(value1.plus(value2).equals(value2.plus(value1), true)); + + // make sure errors happen when they should + assertThrows(IllegalArgumentException.class, () -> value1.plus(value4)); } - + @Test public void testConversion() { final LinearUnit metre = SI.METRE; final Unit inch = metre.times(0.0254); - + + final UnitValue value = UnitValue.of(inch, 75); + assertEquals(1.9, inch.convertTo(metre, 75), 0.01); - + assertEquals(1.9, value.convertTo(metre).getValue(), 0.01); + // try random stuff for (int i = 0; i < 1000; i++) { // initiate random values - final double conversionFactor = rng.nextDouble() * 1000000; - final double testValue = rng.nextDouble() * 1000000; + final double conversionFactor = UnitTest.rng.nextDouble() * 1000000; + final double testValue = UnitTest.rng.nextDouble() * 1000000; final double expected = testValue * conversionFactor; - + // test final Unit unit = SI.METRE.times(conversionFactor); final double actual = unit.convertToBase(testValue); - - assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); + + assertEquals(actual, expected, + expected * DecimalComparison.DOUBLE_EPSILON); } } - + @Test public void testEquals() { final LinearUnit metre = SI.METRE; final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); - + assertEquals(metre, meter); } - + @Test public void testMultiplicationAndDivision() { // test unit-times-unit multiplication - final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + final LinearUnit generatedJoule = SI.KILOGRAM + .times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); final LinearUnit actualJoule = SI.JOULE; - + assertEquals(generatedJoule, actualJoule); - + // test multiplication by conversion factors final LinearUnit kilometre = SI.METRE.times(1000); final LinearUnit hour = SI.SECOND.times(3600); final LinearUnit generatedKPH = kilometre.dividedBy(hour); - + final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); - + assertEquals(generatedKPH, actualKPH); } - + @Test public void testPrefixes() { final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); final LinearUnit actualKilometre = SI.METRE.times(1000); - + assertEquals(generatedKilometre, actualKilometre); } } -- cgit v1.2.3 From 77c7c962a6ed810b12685aa9ace4bd8a62761cea Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Aug 2020 16:08:22 -0500 Subject: Made UnitConverterGUI use UnitValue and LinearUnitValue --- .../converterGUI/UnitConverterGUI.java | 687 +++++++----- src/org/unitConverter/unit/LinearUnitValue.java | 12 + src/org/unitConverter/unit/UnitDatabase.java | 1163 ++++++++++++-------- 3 files changed, 1119 insertions(+), 743 deletions(-) (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 986cb52..8c70df4 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -59,10 +59,13 @@ import org.unitConverter.math.ObjectProduct; import org.unitConverter.unit.BaseDimension; import org.unitConverter.unit.BritishImperial; import org.unitConverter.unit.LinearUnit; +import org.unitConverter.unit.LinearUnitValue; +import org.unitConverter.unit.NameSymbol; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitDatabase; import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.unit.UnitValue; /** * @author Adrien Hopkins @@ -74,20 +77,10 @@ final class UnitConverterGUI { * A tab in the View. */ private enum Pane { - UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, SETTINGS; + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, + SETTINGS; } - /** - * Different types of rounding. - * - * Significant digits: Rounds to a number of digits. i.e. with precision 5, 12345.6789 rounds to 12346. - * Decimal places: Rounds to a number of digits after the decimal point, i.e. with precision 5, 12345.6789 rounds to 12345.67890. - * Scientific: Rounds based on the number of digits and operations, following standard scientific rounding. - */ - private static enum RoundingType { - SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; - } - private static class Presenter { /** * Adds default units and dimensions to a database. @@ -110,40 +103,40 @@ final class UnitConverterGUI { // nonlinear units - must be loaded manually database.addUnit("tempCelsius", SI.CELSIUS); database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); - + // load initial dimensions database.addDimension("LENGTH", SI.Dimensions.LENGTH); database.addDimension("MASS", SI.Dimensions.MASS); database.addDimension("TIME", SI.Dimensions.TIME); database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } - + /** The presenter's associated view. */ private final View view; - + /** The units known by the program. */ private final UnitDatabase database; - + /** The names of all of the units */ private final List unitNames; - + /** The names of all of the prefixes */ private final List prefixNames; - + /** The names of all of the dimensions */ private final List dimensionNames; - + private final Comparator prefixNameComparator; - + /* - * Rounding-related settings. I am using my own system, and not MathContext, - * because MathContext does not support decimal place based or scientific - * rounding, only significant digit based rounding. + * Rounding-related settings. I am using my own system, and not + * MathContext, because MathContext does not support decimal place based + * or scientific rounding, only significant digit based rounding. */ private int precision = 6; - + private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; - + /** * Creates the presenter. * @@ -153,14 +146,14 @@ final class UnitConverterGUI { */ Presenter(final View view) { this.view = view; - + // load initial units this.database = new UnitDatabase(); Presenter.addDefaults(this.database); - + this.database.loadUnitsFile(new File("unitsfile.txt")); this.database.loadDimensionFile(new File("dimensionfile.txt")); - + // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. // otherwise, they are compared by value @@ -169,37 +162,43 @@ final class UnitConverterGUI { return -1; else if (!Presenter.this.database.containsPrefixName(o2)) return 1; - + final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); - + if (p1.getMultiplier() < p2.getMultiplier()) return -1; else if (p1.getMultiplier() > p2.getMultiplier()) return 1; - + return o1.compareTo(o2); }; - - this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet()); + + this.unitNames = new ArrayList<>( + this.database.unitMapPrefixless().keySet()); this.unitNames.sort(null); // sorts it using Comparable - + this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); - this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator - - this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet())); + this.prefixNames.sort(this.prefixNameComparator); // sorts it using my + // comparator + + this.dimensionNames = new DelegateListModel<>( + new ArrayList<>(this.database.dimensionMap().keySet())); this.dimensionNames.sort(null); // sorts it using Comparable - + // a Predicate that returns true iff the argument is a full base unit - final Predicate isFullBase = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); - + final Predicate isFullBase = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + // print out unit counts - System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n", + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", new HashSet<>(this.database.unitMapPrefixless().values()).size(), this.database.unitMapPrefixless().size(), - new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count()); + new HashSet<>(this.database.unitMapPrefixless().values()) + .stream().filter(isFullBase).count()); } - + /** * Converts in the dimension-based converter * @@ -209,7 +208,8 @@ final class UnitConverterGUI { public final void convertDimensionBased() { final String fromSelection = this.view.getFromSelection(); if (fromSelection == null) { - this.view.showErrorDialog("Error", "No unit selected in From field"); + this.view.showErrorDialog("Error", + "No unit selected in From field"); return; } final String toSelection = this.view.getToSelection(); @@ -217,31 +217,35 @@ final class UnitConverterGUI { this.view.showErrorDialog("Error", "No unit selected in To field"); return; } - + final Unit from = this.database.getUnit(fromSelection); - final Unit to = this.database.getUnit(toSelection); - - final double beforeValue; + final Unit to = this.database.getUnit(toSelection) + .withName(NameSymbol.ofName(toSelection)); + + final UnitValue beforeValue; try { - beforeValue = this.view.getDimensionConverterInput(); - } catch (ParseException e) { - this.view.showErrorDialog("Error", "Error in parsing: " + e.getMessage()); + beforeValue = UnitValue.of(from, + this.view.getDimensionConverterInput()); + } catch (final ParseException e) { + this.view.showErrorDialog("Error", + "Error in parsing: " + e.getMessage()); return; } - final double value = from.convertTo(to, beforeValue); - - final String output = this.getRoundedString(new BigDecimal(value)); - - this.view.setDimensionConverterOutputText(String.format("%s %s = %s %s", - this.view.getDimensionConverterText(), fromSelection, output, toSelection)); + final UnitValue value = beforeValue.convertTo(to); + + final String output = this.getRoundedString(value); + + this.view.setDimensionConverterOutputText( + String.format("%s = %s", beforeValue, output)); } - + /** * Runs whenever the convert button is pressed. * *

- * Reads and parses a unit expression from the from and to boxes, then converts - * {@code from} to {@code to}. Any errors are shown in JOptionPanes. + * Reads and parses a unit expression from the from and to boxes, then + * converts {@code from} to {@code to}. Any errors are shown in + * JOptionPanes. *

* * @since 2019-01-26 @@ -250,64 +254,78 @@ final class UnitConverterGUI { public final void convertExpressions() { final String fromUnitString = this.view.getFromText(); final String toUnitString = this.view.getToText(); - + if (fromUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box."); + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the From: box."); return; } if (toUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the To: box."); + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the To: box."); return; } - - // try to parse from - final Unit from; + + final LinearUnitValue from; + final Unit to; try { - from = this.database.getUnitFromExpression(fromUnitString); + from = this.database.evaluateUnitExpression(fromUnitString); } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); + this.view.showErrorDialog("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); return; } - - final double value; - // try to parse to - final Unit to; try { - if (this.database.containsUnitName(toUnitString)) { - // if it's a unit, convert to that - to = this.database.getUnit(toUnitString); - } else { - to = this.database.getUnitFromExpression(toUnitString); - } + to = this.database.getUnitFromExpression(toUnitString); } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); + this.view.showErrorDialog("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); return; } - - if (from.canConvertTo(to)) { - value = from.convertTo(to, 1); - - // round value - final String output = this.getRoundedString(new BigDecimal(value)); - - this.view.setExpressionConverterOutputText( - String.format("%s = %s %s", fromUnitString, output, toUnitString)); - } else if (from instanceof LinearUnit && SI.ONE.dividedBy((LinearUnit) from).canConvertTo(to)) { - // reciprocal conversion (like seconds to hertz) - value = SI.ONE.dividedBy((LinearUnit) from).convertTo(to, 1); - - // round value - final String output = this.getRoundedString(new BigDecimal(value)); - + + if (to instanceof LinearUnit) { + // convert to LinearUnitValue + final LinearUnitValue from2; + final LinearUnit to2 = (LinearUnit) to; + final boolean useSlash; + + if (from.canConvertTo(to2)) { + from2 = from; + useSlash = false; + } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { + from2 = LinearUnitValue.ONE.dividedBy(from); + useSlash = true; + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + return; + } + + final LinearUnitValue converted = from2.convertTo(to2); this.view.setExpressionConverterOutputText( - String.format("1 / %s = %s %s", fromUnitString, output, toUnitString)); + (useSlash ? "1 / " : "") + String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + return; } else { - // if I can't convert, leave - this.view.showErrorDialog("Conversion Error", - String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); + // convert to UnitValue + final UnitValue from2 = from.asUnitValue(); + if (from2.canConvertTo(to)) { + final UnitValue converted = from2.convertTo(to); + + this.view + .setExpressionConverterOutputText(String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + } } } - + /** * @return a list of all of the unit dimensions * @since 2019-04-13 @@ -316,7 +334,7 @@ final class UnitConverterGUI { public final List dimensionNameList() { return this.dimensionNames; } - + /** * @return a comparator to compare prefix names * @since 2019-04-14 @@ -325,20 +343,21 @@ final class UnitConverterGUI { public final Comparator getPrefixNameComparator() { return this.prefixNameComparator; } - + /** * @param value value to round - * @return string of that value rounded to {@code significantDigits} significant - * digits. + * @return string of that value rounded to {@code significantDigits} + * significant digits. * @since 2019-04-14 * @since v0.2.0 */ private final String getRoundedString(final BigDecimal value) { // round value based on rounding type final BigDecimal roundedValue; - switch (roundingType) { + switch (this.roundingType) { case DECIMAL_PLACES: - roundedValue = value.setScale(precision, RoundingMode.HALF_EVEN); + roundedValue = value.setScale(this.precision, + RoundingMode.HALF_EVEN); break; case SCIENTIFIC: throw new UnsupportedOperationException("Not yet implemented."); @@ -350,7 +369,7 @@ final class UnitConverterGUI { } String output = roundedValue.toString(); - + // remove trailing zeroes if (output.contains(".")) { while (output.endsWith("0")) { @@ -360,10 +379,68 @@ final class UnitConverterGUI { output = output.substring(0, output.length() - 1); } } - + return output; } - + + /** + * Like {@link LinearUnitValue#toString(boolean)} with parameter + * {@code false}, but obeys this unit converter's rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final LinearUnitValue value) { + switch (this.roundingType) { + case DECIMAL_PLACES: + case SIGNIFICANT_DIGITS: + return this.getRoundedString(value.asUnitValue()); + case SCIENTIFIC: + return value.toString(false); + default: + throw new AssertionError("Invalid switch condition."); + } + } + + /** + * Like {@link UnitValue#toString()}, but obeys this unit converter's + * rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final UnitValue value) { + final BigDecimal unrounded = new BigDecimal(value.getValue()); + final BigDecimal rounded; + int precision = this.precision; + switch (this.roundingType) { + case DECIMAL_PLACES: + rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); + break; + case SCIENTIFIC: + precision = 12; + //$FALL-THROUGH$ + case SIGNIFICANT_DIGITS: + rounded = unrounded + .round(new MathContext(precision, RoundingMode.HALF_EVEN)); + break; + default: + throw new AssertionError("Invalid switch condition."); + } + + String output = rounded.toString(); + + // remove trailing zeroes + if (output.contains(".")) { + while (output.endsWith("0")) { + output = output.substring(0, output.length() - 1); + } + if (output.endsWith(".")) { + output = output.substring(0, output.length() - 1); + } + } + + return output + " " + value.getUnit().getPrimaryName().get(); + } + /** * @return a set of all prefix names in the database * @since 2019-04-14 @@ -372,7 +449,7 @@ final class UnitConverterGUI { public final Set prefixNameSet() { return this.database.prefixMap().keySet(); } - + /** * Runs whenever a prefix is selected in the viewer. *

@@ -388,11 +465,12 @@ final class UnitConverterGUI { return; else { final UnitPrefix prefix = this.database.getPrefix(prefixName); - - this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); + + this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", + prefixName, prefix.getMultiplier())); } } - + /** * @param precision new value of precision * @since 2019-01-15 @@ -401,10 +479,18 @@ final class UnitConverterGUI { public final void setPrecision(final int precision) { this.precision = precision; } - + + /** + * @param roundingType the roundingType to set + * @since 2020-07-16 + */ + public final void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + } + /** - * Returns true if and only if the unit represented by {@code unitName} has the - * dimension represented by {@code dimensionName}. + * Returns true if and only if the unit represented by {@code unitName} + * has the dimension represented by {@code dimensionName}. * * @param unitName name of unit to test * @param dimensionName name of dimension to test @@ -412,12 +498,14 @@ final class UnitConverterGUI { * @since 2019-04-13 * @since v0.2.0 */ - public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { + public final boolean unitMatchesDimension(final String unitName, + final String dimensionName) { final Unit unit = this.database.getUnit(unitName); - final ObjectProduct dimension = this.database.getDimension(dimensionName); + final ObjectProduct dimension = this.database + .getDimension(dimensionName); return unit.getDimension().equals(dimension); } - + /** * Runs whenever a unit is selected in the viewer. *

@@ -433,11 +521,11 @@ final class UnitConverterGUI { return; else { final Unit unit = this.database.getUnit(unitName); - + this.view.setUnitTextBoxText(unit.toString()); } } - + /** * @return a set of all of the unit names * @since 2019-04-14 @@ -446,16 +534,21 @@ final class UnitConverterGUI { public final Set unitNameSet() { return this.database.unitMapPrefixless().keySet(); } - - /** - * @param roundingType the roundingType to set - * @since 2020-07-16 - */ - public final void setRoundingType(RoundingType roundingType) { - this.roundingType = roundingType; - } } - + + /** + * Different types of rounding. + * + * Significant digits: Rounds to a number of digits. i.e. with precision 5, + * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits + * after the decimal point, i.e. with precision 5, 12345.6789 rounds to + * 12345.67890. Scientific: Rounds based on the number of digits and + * operations, following standard scientific rounding. + */ + private static enum RoundingType { + SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; + } + private static class View { private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); @@ -465,7 +558,7 @@ final class UnitConverterGUI { private final Presenter presenter; /** The master pane containing all of the tabs. */ private final JTabbedPane masterPane; - + // DIMENSION-BASED CONVERTER /** The panel for inputting values in the dimension-based converter */ private final JTextField valueInput; @@ -475,7 +568,7 @@ final class UnitConverterGUI { private final SearchBoxList toSearch; /** The output area in the dimension-based converter */ private final JTextArea dimensionBasedOutput; - + // EXPRESSION-BASED CONVERTER /** The "From" entry in the conversion panel */ private final JTextField fromEntry; @@ -483,7 +576,7 @@ final class UnitConverterGUI { private final JTextField toEntry; /** The output area in the conversion panel */ private final JTextArea output; - + // UNIT AND PREFIX VIEWERS /** The searchable list of unit names in the unit viewer */ private final SearchBoxList unitNameList; @@ -493,7 +586,7 @@ final class UnitConverterGUI { private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; - + /** * Creates the {@code View}. * @@ -505,7 +598,7 @@ final class UnitConverterGUI { this.frame = new JFrame("Unit Converter"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); this.masterPane = new JTabbedPane(); - + // create the components this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), @@ -519,13 +612,13 @@ final class UnitConverterGUI { this.fromEntry = new JTextField(); this.toEntry = new JTextField(); this.output = new JTextArea(2, 32); - + // create more components this.initComponents(); - + this.frame.pack(); } - + /** * @return the currently selected pane. * @throws AssertionError if no pane (or an invalid pane) is selected @@ -546,19 +639,20 @@ final class UnitConverterGUI { throw new AssertionError("No selected pane, or invalid pane."); } } - + /** * @return value in dimension-based converter - * @throws ParseException + * @throws ParseException * @since 2020-07-07 */ public double getDimensionConverterInput() throws ParseException { - Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); - if (value instanceof Double) { + final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); + if (value instanceof Double) return (double) value; - } else if (value instanceof Long) { - return (((Long) value).longValue()); - } else throw new AssertionError(); + else if (value instanceof Long) + return ((Long) value).longValue(); + else + throw new AssertionError(); } /** @@ -569,7 +663,7 @@ final class UnitConverterGUI { public String getDimensionConverterText() { return this.valueInput.getText(); } - + /** * @return selection in "From" selector in dimension-based converter * @since 2019-04-13 @@ -578,7 +672,7 @@ final class UnitConverterGUI { public String getFromSelection() { return this.fromSearch.getSelectedValue(); } - + /** * @return text in "From" box in converter panel * @since 2019-01-15 @@ -587,7 +681,7 @@ final class UnitConverterGUI { public String getFromText() { return this.fromEntry.getText(); } - + /** * @return index of selected prefix in prefix viewer * @since 2019-01-15 @@ -596,7 +690,7 @@ final class UnitConverterGUI { public String getPrefixViewerSelection() { return this.prefixNameList.getSelectedValue(); } - + /** * @return selection in "To" selector in dimension-based converter * @since 2019-04-13 @@ -605,7 +699,7 @@ final class UnitConverterGUI { public String getToSelection() { return this.toSearch.getSelectedValue(); } - + /** * @return text in "To" box in converter panel * @since 2019-01-26 @@ -614,7 +708,7 @@ final class UnitConverterGUI { public String getToText() { return this.toEntry.getText(); } - + /** * @return index of selected unit in unit viewer * @since 2019-01-15 @@ -623,7 +717,7 @@ final class UnitConverterGUI { public String getUnitViewerSelection() { return this.unitNameList.getSelectedValue(); } - + /** * Starts up the application. * @@ -633,7 +727,7 @@ final class UnitConverterGUI { public final void init() { this.frame.setVisible(true); } - + /** * Initializes the view's components. * @@ -643,199 +737,213 @@ final class UnitConverterGUI { private final void initComponents() { final JPanel masterPanel = new JPanel(); this.frame.add(masterPanel); - + masterPanel.setLayout(new BorderLayout()); - + { // pane with all of the tabs masterPanel.add(this.masterPane, BorderLayout.CENTER); - + { // a panel for unit conversion using a selector final JPanel convertUnitPanel = new JPanel(); this.masterPane.addTab("Convert Units", convertUnitPanel); this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); - + convertUnitPanel.setLayout(new BorderLayout()); - + { // panel for input part final JPanel inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); - + inputPanel.setLayout(new GridLayout(1, 3)); - + final JComboBox dimensionSelector = new JComboBox<>( - this.presenter.dimensionNameList().toArray(new String[0])); + this.presenter.dimensionNameList() + .toArray(new String[0])); dimensionSelector.setSelectedItem("LENGTH"); - + // handle dimension filter - final MutablePredicate dimensionFilter = new MutablePredicate<>(s -> true); - + final MutablePredicate dimensionFilter = new MutablePredicate<>( + s -> true); + // panel for From things inputPanel.add(this.fromSearch); - + this.fromSearch.addSearchFilter(dimensionFilter); - - { // for dimension selector and arrow that represents conversion + + { // for dimension selector and arrow that represents + // conversion final JPanel inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); - + inBetweenPanel.setLayout(new BorderLayout()); - + { // dimension selector - inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); + inBetweenPanel.add(dimensionSelector, + BorderLayout.PAGE_START); } - + { // the arrow in the middle final JLabel arrowLabel = new JLabel("->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); } } - + // panel for To things - + inputPanel.add(this.toSearch); - + this.toSearch.addSearchFilter(dimensionFilter); - + // code for dimension filter dimensionSelector.addItemListener(e -> { - dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); }); - - // apply the item listener once because I have a default selection - dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); + + // apply the item listener once because I have a default + // selection + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); } - + { // panel for submit and output, and also value entry final JPanel outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); - + outputPanel.setLayout(new GridLayout(3, 1)); - + { // unit input final JPanel valueInputPanel = new JPanel(); outputPanel.add(valueInputPanel); - + valueInputPanel.setLayout(new BorderLayout()); - + { // prompt - final JLabel valuePrompt = new JLabel("Value to convert: "); - valueInputPanel.add(valuePrompt, BorderLayout.LINE_START); + final JLabel valuePrompt = new JLabel( + "Value to convert: "); + valueInputPanel.add(valuePrompt, + BorderLayout.LINE_START); } - + { // value to convert - valueInputPanel.add(this.valueInput, BorderLayout.CENTER); + valueInputPanel.add(this.valueInput, + BorderLayout.CENTER); } } - + { // button to convert final JButton convertButton = new JButton("Convert"); outputPanel.add(convertButton); - - convertButton.addActionListener(e -> this.presenter.convertDimensionBased()); + + convertButton.addActionListener( + e -> this.presenter.convertDimensionBased()); convertButton.setMnemonic(KeyEvent.VK_ENTER); } - + { // output of conversion outputPanel.add(this.dimensionBasedOutput); this.dimensionBasedOutput.setEditable(false); } } } - + { // panel for unit conversion using expressions final JPanel convertExpressionPanel = new JPanel(); - this.masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); - + convertExpressionPanel.setLayout(new GridLayout(4, 1)); - + { // panel for units to convert from final JPanel fromPanel = new JPanel(); convertExpressionPanel.add(fromPanel); - + fromPanel.setBorder(BorderFactory.createTitledBorder("From")); fromPanel.setLayout(new GridLayout(1, 1)); - + { // entry for units fromPanel.add(this.fromEntry); } } - + { // panel for units to convert to final JPanel toPanel = new JPanel(); convertExpressionPanel.add(toPanel); - + toPanel.setBorder(BorderFactory.createTitledBorder("To")); toPanel.setLayout(new GridLayout(1, 1)); - + { // entry for units toPanel.add(this.toEntry); } } - + { // button to convert final JButton convertButton = new JButton("Convert"); convertExpressionPanel.add(convertButton); - - convertButton.addActionListener(e -> this.presenter.convertExpressions()); + + convertButton.addActionListener( + e -> this.presenter.convertExpressions()); convertButton.setMnemonic(KeyEvent.VK_ENTER); } - + { // output of conversion final JPanel outputPanel = new JPanel(); convertExpressionPanel.add(outputPanel); - - outputPanel.setBorder(BorderFactory.createTitledBorder("Output")); + + outputPanel + .setBorder(BorderFactory.createTitledBorder("Output")); outputPanel.setLayout(new GridLayout(1, 1)); - + { // output outputPanel.add(this.output); this.output.setEditable(false); } } } - + { // panel to look up units final JPanel unitLookupPanel = new JPanel(); this.masterPane.addTab("Unit Viewer", unitLookupPanel); this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); - + unitLookupPanel.setLayout(new GridLayout()); - + { // search panel unitLookupPanel.add(this.unitNameList); - - this.unitNameList.getSearchList() - .addListSelectionListener(e -> this.presenter.unitNameSelected()); + + this.unitNameList.getSearchList().addListSelectionListener( + e -> this.presenter.unitNameSelected()); } - + { // the text box for unit's toString unitLookupPanel.add(this.unitTextBox); this.unitTextBox.setEditable(false); this.unitTextBox.setLineWrap(true); } } - + { // panel to look up prefixes final JPanel prefixLookupPanel = new JPanel(); this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); - + prefixLookupPanel.setLayout(new GridLayout(1, 2)); - + { // panel for listing and seaching prefixLookupPanel.add(this.prefixNameList); - - this.prefixNameList.getSearchList() - .addListSelectionListener(e -> this.presenter.prefixSelected()); + + this.prefixNameList.getSearchList().addListSelectionListener( + e -> this.presenter.prefixSelected()); } - + { // the text box for prefix's toString prefixLookupPanel.add(this.prefixTextBox); this.prefixTextBox.setEditable(false); @@ -848,77 +956,104 @@ final class UnitConverterGUI { this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); this.masterPane.setMnemonicAt(4, KeyEvent.VK_S); - settingsPanel.setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + settingsPanel.setLayout( + new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); { // rounding settings final JPanel roundingPanel = new JPanel(); settingsPanel.add(roundingPanel); - roundingPanel.setBorder(new TitledBorder("Rounding Settings")); + roundingPanel + .setBorder(new TitledBorder("Rounding Settings")); roundingPanel.setLayout(new GridBagLayout()); // rounding rule selection final ButtonGroup roundingRuleButtons = new ButtonGroup(); final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); - roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton fixedPrecision = new JRadioButton("Fixed Precision"); + final JRadioButton fixedPrecision = new JRadioButton( + "Fixed Precision"); fixedPrecision.setSelected(true); - fixedPrecision.addActionListener(e -> this.presenter.setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + fixedPrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); roundingRuleButtons.add(fixedPrecision); - roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton fixedDecimals = new JRadioButton("Fixed Decimal Places"); - fixedDecimals.addActionListener(e -> this.presenter.setRoundingType(RoundingType.DECIMAL_PLACES)); + final JRadioButton fixedDecimals = new JRadioButton( + "Fixed Decimal Places"); + fixedDecimals.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.DECIMAL_PLACES)); roundingRuleButtons.add(fixedDecimals); - roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton relativePrecision = new JRadioButton("Scientific Precision"); + final JRadioButton relativePrecision = new JRadioButton( + "Scientific Precision"); relativePrecision.setEnabled(false); - relativePrecision.addActionListener(e -> this.presenter.setRoundingType(RoundingType.SCIENTIFIC)); + relativePrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); - roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); final JLabel sliderLabel = new JLabel("Precision:"); - roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4).setAnchor(GridBagConstraints.LINE_START).build()); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); final JSlider sigDigSlider = new JSlider(0, 12); - roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5).setAnchor(GridBagConstraints.LINE_START).build()); - + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + sigDigSlider.setMajorTickSpacing(4); sigDigSlider.setMinorTickSpacing(1); sigDigSlider.setSnapToTicks(true); sigDigSlider.setPaintTicks(true); sigDigSlider.setPaintLabels(true); - - sigDigSlider.addChangeListener( - e -> this.presenter.setPrecision(sigDigSlider.getValue())); + + sigDigSlider.addChangeListener(e -> this.presenter + .setPrecision(sigDigSlider.getValue())); } { // prefix repetition settings final JPanel prefixRepetitionPanel = new JPanel(); settingsPanel.add(prefixRepetitionPanel); - prefixRepetitionPanel.setBorder(new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setBorder( + new TitledBorder("Prefix Repetition Settings")); prefixRepetitionPanel.setLayout(new GridBagLayout()); // prefix rules final ButtonGroup prefixRuleButtons = new ButtonGroup(); - final JRadioButton noRepetition = new JRadioButton("No Repetition"); + final JRadioButton noRepetition = new JRadioButton( + "No Repetition"); noRepetition.setEnabled(false); prefixRuleButtons.add(noRepetition); - prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + prefixRepetitionPanel.add(noRepetition, + new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START) + .build()); - final JRadioButton noRestriction = new JRadioButton("No Restriction"); + final JRadioButton noRestriction = new JRadioButton( + "No Restriction"); noRestriction.setSelected(true); noRestriction.setEnabled(false); prefixRuleButtons.add(noRestriction); - prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + prefixRepetitionPanel.add(noRestriction, + new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START) + .build()); - final JRadioButton customRepetition = new JRadioButton("Custom Repetition Rule"); + final JRadioButton customRepetition = new JRadioButton( + "Custom Repetition Rule"); customRepetition.setEnabled(false); prefixRuleButtons.add(customRepetition); - prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + prefixRepetitionPanel.add(customRepetition, + new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START) + .build()); } { // search settings @@ -930,46 +1065,59 @@ final class UnitConverterGUI { // searching rules final ButtonGroup searchRuleButtons = new ButtonGroup(); - final JRadioButton noPrefixes = new JRadioButton("Never Include Prefixed Units"); + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); noPrefixes.setEnabled(false); searchRuleButtons.add(noPrefixes); - searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton fixedPrefixes = new JRadioButton("Include Some Prefixes"); + final JRadioButton fixedPrefixes = new JRadioButton( + "Include Some Prefixes"); fixedPrefixes.setEnabled(false); searchRuleButtons.add(fixedPrefixes); - searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton explicitPrefixes = new JRadioButton("Include Explicit Prefixes"); + final JRadioButton explicitPrefixes = new JRadioButton( + "Include Explicit Prefixes"); explicitPrefixes.setEnabled(false); searchRuleButtons.add(explicitPrefixes); - searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JRadioButton alwaysInclude = new JRadioButton("Include All Single Prefixes"); + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); alwaysInclude.setEnabled(false); searchRuleButtons.add(alwaysInclude); - searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3).setAnchor(GridBagConstraints.LINE_START).build()); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); } { // miscellaneous settings final JPanel miscPanel = new JPanel(); settingsPanel.add(miscPanel); - miscPanel.setBorder(new TitledBorder("Miscellaneous Settings")); + miscPanel + .setBorder(new TitledBorder("Miscellaneous Settings")); miscPanel.setLayout(new GridBagLayout()); - final JCheckBox showAllVariations = new JCheckBox("Show Symbols in \"Convert Units\""); + final JCheckBox showAllVariations = new JCheckBox( + "Show Symbols in \"Convert Units\""); showAllVariations.setSelected(true); showAllVariations.setEnabled(false); - miscPanel.add(showAllVariations, new GridBagBuilder(0, 0).setAnchor(GridBagConstraints.LINE_START).build()); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); - final JButton unitFileButton = new JButton("Manage Unit Data Files"); + final JButton unitFileButton = new JButton( + "Manage Unit Data Files"); unitFileButton.setEnabled(false); - miscPanel.add(unitFileButton, new GridBagBuilder(0, 1).setAnchor(GridBagConstraints.LINE_START).build()); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); } } } } - + /** * Sets the text in the output of the dimension-based converter. * @@ -980,7 +1128,7 @@ final class UnitConverterGUI { public void setDimensionConverterOutputText(final String text) { this.dimensionBasedOutput.setText(text); } - + /** * Sets the text in the output of the conversion panel. * @@ -991,7 +1139,7 @@ final class UnitConverterGUI { public void setExpressionConverterOutputText(final String text) { this.output.setText(text); } - + /** * Sets the text of the prefix text box in the prefix viewer. * @@ -1002,7 +1150,7 @@ final class UnitConverterGUI { public void setPrefixTextBoxText(final String text) { this.prefixTextBox.setText(text); } - + /** * Sets the text of the unit text box in the unit viewer. * @@ -1013,7 +1161,7 @@ final class UnitConverterGUI { public void setUnitTextBoxText(final String text) { this.unitTextBox.setText(text); } - + /** * Shows an error dialog. * @@ -1023,10 +1171,11 @@ final class UnitConverterGUI { * @since v0.1.0 */ public void showErrorDialog(final String title, final String message) { - JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); } } - + public static void main(final String[] args) { new View().init(); } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 74b0400..7096738 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -20,6 +20,8 @@ import org.unitConverter.math.DecimalComparison; * @since 2020-07-26 */ public final class LinearUnitValue { + public static final LinearUnitValue ONE = getExact(SI.ONE, 1); + /** * Gets an exact {@code LinearUnitValue} * @@ -81,6 +83,15 @@ public final class LinearUnitValue { this.uncertainty = uncertainty; } + /** + * @return this value as a {@code UnitValue}. All uncertainty information is + * removed from the returned value. + * @since 2020-08-04 + */ + public final UnitValue asUnitValue() { + return UnitValue.of(this.unit, this.value); + } + /** * @param other a {@code LinearUnit} * @return true iff this value can be represented with {@code other}. @@ -135,6 +146,7 @@ public final class LinearUnitValue { * km) returns true. * * @since 2020-07-26 + * @see #equals(Object, boolean) */ @Override public boolean equals(final Object obj) { diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 507266d..0246630 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -55,29 +56,33 @@ public final class UnitDatabase { /** * A map for units that allows the use of prefixes. *

- * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is - * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the - * maps that were passed in during construction. + * As this map implementation is intended to be used as a sort of "augmented + * view" of a unit and prefix map, it is unmodifiable but instead reflects + * the changes to the maps passed into it. Do not edit this map, instead edit + * the maps that were passed in during construction. *

*

* The rules for applying prefixes onto units are the following: *

    *
  • Prefixes can only be applied to linear units.
  • - *
  • Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if - * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with - * the prefix "A", even though they have the same string.
  • - *
  • Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and - * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • + *
  • Before attempting to search for prefixes in a unit name, this map will + * first search for a unit name. So, if there are two units, "B" and "AB", + * and a prefix "A", this map will favour the unit "AB" over the unit "B" + * with the prefix "A", even though they have the same string.
  • + *
  • Longer prefixes are preferred to shorter prefixes. So, if you have + * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return + * the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • *
*

*

- * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some - * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an + * This map is infinite in size if there is at least one unit and at least + * one prefix. If it is infinite, some operations that only work with finite + * collections, like converting name/entry sets to arrays, will throw an * {@code IllegalStateException}. *

*

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), {@link #containsValue} and {@link #values()} - * currently ignore prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), + * {@link #containsValue} and {@link #values()} currently ignore prefixes. *

* * @author Adrien Hopkins @@ -89,16 +94,19 @@ public final class UnitDatabase { * The class used for entry sets. * *

- * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this - * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. + * If the map that created this set is infinite in size (has at least one + * unit and at least one prefix), this set is infinite as well. If this + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitEntrySet extends AbstractSet> { + private static final class PrefixedUnitEntrySet + extends AbstractSet> { /** * The entry for this set. * @@ -106,17 +114,16 @@ public final class UnitDatabase { * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntry implements Entry { + private static final class PrefixedUnitEntry + implements Entry { private final String key; private final Unit value; - + /** * Creates the {@code PrefixedUnitEntry}. * - * @param key - * key - * @param value - * value + * @param key key + * @param value value * @since 2019-04-14 * @since v0.2.0 */ @@ -124,7 +131,7 @@ public final class UnitDatabase { this.key = key; this.value = value; } - + /** * @since 2019-05-03 */ @@ -136,34 +143,38 @@ public final class UnitDatabase { return Objects.equals(this.getKey(), other.getKey()) && Objects.equals(this.getValue(), other.getValue()); } - + @Override public String getKey() { return this.key; } - + @Override public Unit getValue() { return this.value; } - + /** * @since 2019-05-03 */ @Override public int hashCode() { return (this.getKey() == null ? 0 : this.getKey().hashCode()) - ^ (this.getValue() == null ? 0 : this.getValue().hashCode()); + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); } - + @Override public Unit setValue(final Unit value) { - throw new UnsupportedOperationException("Cannot set value in an immutable entry"); + throw new UnsupportedOperationException( + "Cannot set value in an immutable entry"); } - + /** - * Returns a string representation of the entry. The format of the string is the string representation - * of the key, then the equals ({@code =}) character, then the string representation of the value. + * Returns a string representation of the entry. The format of the + * string is the string representation of the key, then the equals + * ({@code =}) character, then the string representation of the + * value. * * @since 2019-05-03 */ @@ -172,27 +183,30 @@ public final class UnitDatabase { return this.getKey() + "=" + this.getValue(); } } - + /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntryIterator implements Iterator> { + private static final class PrefixedUnitEntryIterator + implements Iterator> { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); - + // values from the unit entry set private final Map map; private transient final List unitNames; private transient final List prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -202,7 +216,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -214,10 +228,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -229,7 +243,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -238,127 +252,142 @@ public final class UnitDatabase { */ private void incrementPosition() { this.unitNamePosition++; - + if (this.unitNamePosition >= this.unitNames.size()) { // we have used all of our units, go to a different prefix this.unitNamePosition = 0; - + // if the prefix coordinates are empty, then set it to [0] if (this.prefixCoordinates.isEmpty()) { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + if (i < 0) { // we need to add a new coordinate this.prefixCoordinates.add(0, 0); } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); } } } } } - + @Override public Entry next() { // get next element final Entry nextEntry = this.peek(); - + // iterate to next position this.incrementPosition(); - + return nextEntry; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private Entry peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); - + // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // since all of the unprefixed stuff is done, just remove + // nonlinear units if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { this.unitNames.remove(this.unitNamePosition); } } - + final String nextName = this.getCurrentUnitName(); - + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); } - + /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. * * @since 2019-05-03 */ @Override public String toString() { - return String.format("Iterator iterating over name-unit entries; next value is \"%s\"", + return String.format( + "Iterator iterating over name-unit entries; next value is \"%s\"", this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * - * @param map - * map that created this set + * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitEntrySet(final PrefixedUnitMap map) { this.map = map; } - + @Override public boolean add(final Map.Entry e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override - public boolean addAll(final Collection> c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + public boolean addAll( + final Collection> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { // get the entry final Entry entry; - + try { - // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown. + // This is OK because I'm in a try-catch block, catching the + // exact exception that would be thrown. @SuppressWarnings("unchecked") final Entry tempEntry = (Entry) o; entry = tempEntry; } catch (final ClassCastException e) { - throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); + throw new IllegalArgumentException( + "Attempted to test for an entry using a non-entry."); } - - return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue()); + + return this.map.containsKey(entry.getKey()) + && this.map.get(entry.getKey()).equals(entry.getValue()); } - + @Override public boolean containsAll(final Collection c) { for (final Object o : c) @@ -366,37 +395,42 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator> iterator() { return new PrefixedUnitEntryIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override - public boolean removeIf(final Predicate> filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + public boolean removeIf( + final Predicate> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -409,10 +443,9 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { @@ -420,12 +453,12 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public T[] toArray(final T[] a) { @@ -433,53 +466,61 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", this.map.units, this.map.prefixes); } } - + /** * The class used for unit name sets. * *

- * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this - * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. + * If the map that created this set is infinite in size (has at least one + * unit and at least one prefix), this set is infinite as well. If this + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitNameSet extends AbstractSet { + private static final class PrefixedUnitNameSet + extends AbstractSet { /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitNameIterator implements Iterator { + private static final class PrefixedUnitNameIterator + implements Iterator { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); - + // values from the unit name set private final Map map; private transient final List unitNames; private transient final List prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -489,7 +530,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -501,10 +542,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -516,7 +557,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -525,109 +566,121 @@ public final class UnitDatabase { */ private void incrementPosition() { this.unitNamePosition++; - + if (this.unitNamePosition >= this.unitNames.size()) { // we have used all of our units, go to a different prefix this.unitNamePosition = 0; - + // if the prefix coordinates are empty, then set it to [0] if (this.prefixCoordinates.isEmpty()) { this.prefixCoordinates.add(0, 0); } else { // get the prefix coordinate to increment, then increment int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + if (i < 0) { // we need to add a new coordinate this.prefixCoordinates.add(0, 0); } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); } } } } } - + @Override public String next() { final String nextName = this.peek(); - + this.incrementPosition(); - + return nextName; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private String peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // since all of the unprefixed stuff is done, just remove + // nonlinear units if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { this.unitNames.remove(this.unitNamePosition); } } - + return this.getCurrentUnitName(); } - + /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. * * @since 2019-05-03 */ @Override public String toString() { - return String.format("Iterator iterating over unit names; next value is \"%s\"", this.peek()); + return String.format( + "Iterator iterating over unit names; next value is \"%s\"", + this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * - * @param map - * map that created this set + * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitNameSet(final PrefixedUnitMap map) { this.map = map; } - + @Override public boolean add(final String e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public boolean addAll(final Collection c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { return this.map.containsKey(o); } - + @Override public boolean containsAll(final Collection c) { for (final Object o : c) @@ -635,37 +688,41 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator iterator() { return new PrefixedUnitNameIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean removeIf(final Predicate filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -678,10 +735,9 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { @@ -689,13 +745,13 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public T[] toArray(final T[] a) { @@ -703,19 +759,21 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", this.map.units, this.map.prefixes); } } - + /** * The units stored in this collection, without prefixes. * @@ -723,7 +781,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * The available prefixes for use. * @@ -731,95 +789,106 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map prefixes; - + // caches private transient Collection values = null; private transient Set keySet = null; private transient Set> entrySet = null; - + /** * Creates the {@code PrefixedUnitMap}. * - * @param units - * map mapping unit names to units - * @param prefixes - * map mapping prefix names to prefixes + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 * @since v0.2.0 */ - public PrefixedUnitMap(final Map units, final Map prefixes) { - // I am making unmodifiable maps to ensure I don't accidentally make changes. + public PrefixedUnitMap(final Map units, + final Map prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. this.units = Collections.unmodifiableMap(units); this.prefixes = Collections.unmodifiableMap(prefixes); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable map"); + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); } - + @Override public Unit compute(final String key, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override - public Unit computeIfAbsent(final String key, final Function mappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + public Unit computeIfAbsent(final String key, + final Function mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public Unit computeIfPresent(final String key, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public boolean containsKey(final Object key) { // First, test if there is a unit with the key if (this.units.containsKey(key)) return true; - + // Next, try to cast it to String if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to test for a unit using a non-string name."); + throw new IllegalArgumentException( + "Attempted to test for a unit using a non-string name."); final String unitName = (String) key; - + // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; int longestLength = 0; - + for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } - + return longestPrefix != null; } - + /** * {@inheritDoc} * *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method only tests for prefixless units. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method only tests for prefixless units. *

*/ @Override public boolean containsValue(final Object value) { return this.units.containsValue(value); } - + @Override public Set> entrySet() { if (this.entrySet == null) { @@ -827,56 +896,62 @@ public final class UnitDatabase { } return this.entrySet; } - + @Override public Unit get(final Object key) { // First, test if there is a unit with the key if (this.units.containsKey(key)) return this.units.get(key); - + // Next, try to cast it to String if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name."); + throw new IllegalArgumentException( + "Attempted to obtain a unit using a non-string name."); final String unitName = (String) key; - + // Then, look for the longest prefix that is attached to a valid unit String longestPrefix = null; int longestLength = 0; - + for (final String prefixName : this.prefixes.keySet()) { // a prefix name is valid if: // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } - + // if none found, returns null if (longestPrefix == null) return null; else { // get necessary data final String rest = unitName.substring(longestLength); - // this cast will not fail because I verified that it would work before selecting this prefix + // this cast will not fail because I verified that it would work + // before selecting this prefix final LinearUnit unit = (LinearUnit) this.get(rest); final UnitPrefix prefix = this.prefixes.get(longestPrefix); - + return unit.withPrefix(prefix); } } - + @Override public boolean isEmpty() { return this.units.isEmpty(); } - + @Override public Set keySet() { if (this.keySet == null) { @@ -884,53 +959,64 @@ public final class UnitDatabase { } return this.keySet; } - + @Override public Unit merge(final String key, final Unit value, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot merge into an immutable map"); + throw new UnsupportedOperationException( + "Cannot merge into an immutable map"); } - + @Override public Unit put(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public void putAll(final Map m) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public Unit putIfAbsent(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); } - + @Override public Unit remove(final Object key) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); } - + @Override public boolean remove(final Object key, final Object value) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); } - + @Override public Unit replace(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override - public boolean replace(final String key, final Unit oldValue, final Unit newValue) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public boolean replace(final String key, final Unit oldValue, + final Unit newValue) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override - public void replaceAll(final BiFunction function) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public void replaceAll( + final BiFunction function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override public int size() { if (this.units.isEmpty()) @@ -943,66 +1029,80 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) return super.toString(); else - return String.format("Infinite map of name-unit entries created from units %s and prefixes %s", + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", this.units, this.prefixes); } - + /** * {@inheritDoc} * *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method ignores prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method ignores prefixes. *

*/ @Override public Collection values() { if (this.values == null) { - this.values = Collections.unmodifiableCollection(this.units.values()); + this.values = Collections + .unmodifiableCollection(this.units.values()); } return this.values; } } - + /** * Replacements done to *all* expression types */ private static final Map EXPRESSION_REPLACEMENTS = new HashMap<>(); - + // add data to expression replacements static { - // place brackets around any expression of the form "number unit", with or without the space + // add spaces around operators + for (final String operator : Arrays.asList("\\*", "/", "\\^")) { + EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), + " " + operator + " "); + } + + // replace multiple spaces with a single space + EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); + // place brackets around any expression of the form "number unit", with or + // without the space EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer - + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers after it + + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers + // after it + "\\s*" // optional space(s) + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters - + "(?!-?\\d)" // no number directly afterwards (avoids matching "1e3") + + "(?!-?\\d)" // no number directly afterwards (avoids matching + // "1e3") ), "\\($1 $2\\)"); } - + /** * A regular expression that separates names and expressions in unit files. */ - private static final Pattern NAME_EXPRESSION = Pattern.compile("(\\S+)\\s+(\\S.*)"); - + private static final Pattern NAME_EXPRESSION = Pattern + .compile("(\\S+)\\s+(\\S.*)"); + /** * The exponent operator * - * @param base - * base of exponentiation - * @param exponentUnit - * exponent + * @param base base of exponentiation + * @param exponentUnit exponent * @return result * @since 2019-04-10 * @since v0.2.0 */ - private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { + private static final LinearUnit exponentiateUnits(final LinearUnit base, + final LinearUnit exponentUnit) { // exponent function - first check if o2 is a number, if (exponentUnit.getBase().equals(SI.ONE.getBase())) { // then check if it is an integer, @@ -1012,12 +1112,39 @@ public final class UnitDatabase { return base.toExponent((int) (exponent + 0.5)); else // not an integer - throw new UnsupportedOperationException("Decimal exponents are currently not supported."); + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); } else // not a number throw new IllegalArgumentException("Exponents must be numbers."); } - + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2020-08-04 + */ + private static final LinearUnitValue exponentiateUnitValues( + final LinearUnitValue base, final LinearUnitValue exponentValue) { + // exponent function - first check if o2 is a number, + if (exponentValue.canConvertTo(SI.ONE)) { + // then check if it is an integer, + final double exponent = exponentValue.getValue(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + /** * The units in this system, excluding prefixes. * @@ -1025,7 +1152,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixlessUnits; - + /** * The unit prefixes in this system. * @@ -1033,7 +1160,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixes; - + /** * The dimensions in this system. * @@ -1041,7 +1168,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map> dimensions; - + /** * A map mapping strings to units (including prefixes) * @@ -1049,7 +1176,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * A parser that can parse unit expressions. * @@ -1059,21 +1186,41 @@ public final class UnitDatabase { private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>( this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*") + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); - + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) + .build(); + + /** + * A parser that can parse unit value expressions. + * + * @since 2020-08-04 + */ + private final ExpressionParser unitValueExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnitValue) + .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .build(); + /** * A parser that can parse unit prefix expressions * * @since 2019-04-13 * @since v0.2.0 */ - private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); - + private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>( + this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + /** * A parser that can parse unit dimension expressions. * @@ -1081,9 +1228,10 @@ public final class UnitDatabase { * @since v0.2.0 */ private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); - + /** * Creates the {@code UnitsDatabase}. * @@ -1096,43 +1244,42 @@ public final class UnitDatabase { this.dimensions = new HashMap<>(); this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); } - + /** * Adds a unit dimension to the database. * - * @param name - * dimension's name - * @param dimension - * dimension to add - * @throws NullPointerException - * if name or dimension is null + * @param name dimension's name + * @param dimension dimension to add + * @throws NullPointerException if name or dimension is null * @since 2019-03-14 * @since v0.2.0 */ - public void addDimension(final String name, final ObjectProduct dimension) { - this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), + public void addDimension(final String name, + final ObjectProduct dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(dimension, "dimension must not be null.")); } - + /** * Adds to the list from a line in a unit dimension file. * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages + * @param line line to look at + * @param lineCounter number of line, for error messages * @since 2019-04-10 * @since v0.2.0 */ - private void addDimensionFromLine(final String line, final long lineCounter) { + private void addDimensionFromLine(final String line, + final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), + lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1141,17 +1288,18 @@ public final class UnitDatabase { lineCounter)); final String name = lineMatcher.group(1); final String expression = lineMatcher.group(2); - + if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); + System.err.printf("Warning - line %d's dimension name ends in a space", + lineCounter); } - + // if expression is "!", search for an existing dimension // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsDimensionName(name)) - throw new IllegalArgumentException( - String.format("! used but no dimension found (line %d).", lineCounter)); + throw new IllegalArgumentException(String.format( + "! used but no dimension found (line %d).", lineCounter)); } else { // it's a unit, get the unit final ObjectProduct dimension; @@ -1161,20 +1309,17 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - + this.addDimension(name, dimension); } } - + /** * Adds a unit prefix to the database. * - * @param name - * prefix's name - * @param prefix - * prefix to add - * @throws NullPointerException - * if name or prefix is null + * @param name prefix's name + * @param prefix prefix to add + * @throws NullPointerException if name or prefix is null * @since 2019-01-14 * @since v0.1.0 */ @@ -1182,43 +1327,41 @@ public final class UnitDatabase { this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(prefix, "prefix must not be null.")); } - + /** * Adds a unit to the database. * - * @param name - * unit's name - * @param unit - * unit to add - * @throws NullPointerException - * if unit is null + * @param name unit's name + * @param unit unit to add + * @throws NullPointerException if unit is null * @since 2019-01-10 * @since v0.1.0 */ public void addUnit(final String name, final Unit unit) { - this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."), + this.prefixlessUnits.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(unit, "unit must not be null.")); } - + /** * Adds to the list from a line in a unit file. * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages + * @param line line to look at + * @param lineCounter number of line, for error messages * @since 2019-04-10 * @since v0.2.0 */ - private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { + private void addUnitOrPrefixFromLine(final String line, + final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), + lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1226,18 +1369,20 @@ public final class UnitDatabase { "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", lineCounter)); final String name = lineMatcher.group(1); - + final String expression = lineMatcher.group(2); - + if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); + System.err.printf("Warning - line %d's unit name ends in a space", + lineCounter); } - + // if expression is "!", search for an existing unit // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsUnitName(name)) - throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter)); + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); } else { if (name.endsWith("-")) { final UnitPrefix prefix; @@ -1257,17 +1402,16 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - + this.addUnit(name, unit); } } } - + /** * Tests if the database has a unit dimension with this name. * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-03-14 * @since v0.2.0 @@ -1275,12 +1419,11 @@ public final class UnitDatabase { public boolean containsDimensionName(final String name) { return this.dimensions.containsKey(name); } - + /** * Tests if the database has a unit prefix with this name. * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1288,12 +1431,12 @@ public final class UnitDatabase { public boolean containsPrefixName(final String name) { return this.prefixes.containsKey(name); } - + /** - * Tests if the database has a unit with this name, taking prefixes into consideration + * Tests if the database has a unit with this name, taking prefixes into + * consideration * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1301,7 +1444,7 @@ public final class UnitDatabase { public boolean containsUnitName(final String name) { return this.units.containsKey(name); } - + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 @@ -1310,7 +1453,50 @@ public final class UnitDatabase { public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } - + + /** + * Evaluates a unit expression, following the same rules as + * {@link #getUnitFromExpression}. + * + * @param expression expression to parse + * @return {@code LinearUnitValue} representing value of expression + * @since 2020-08-04 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getLinearUnitValue(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + // FIXME the previous operaton also breaks stuff like "1e-5" + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitValueExpressionParser.parseExpression(modifiedExpression); + } + /** * Gets a unit dimension from the database using its name. * @@ -1318,8 +1504,7 @@ public final class UnitDatabase { * This method accepts exponents, like "L^3" *

* - * @param name - * dimension's name + * @param name dimension's name * @return dimension * @since 2019-03-14 * @since v0.2.0 @@ -1328,86 +1513,87 @@ public final class UnitDatabase { Objects.requireNonNull(name, "name must not be null."); if (name.contains("^")) { final String[] baseAndExponent = name.split("\\^"); - - final ObjectProduct base = this.getDimension(baseAndExponent[0]); - + + final ObjectProduct base = this + .getDimension(baseAndExponent[0]); + final int exponent; try { - exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); + exponent = Integer + .parseInt(baseAndExponent[baseAndExponent.length - 1]); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Exponent must be an integer."); } - + return base.toExponent(exponent); } return this.dimensions.get(name); } - + /** * Uses the database's data to parse an expression into a unit dimension *

* The expression is a series of any of the following: *

    - *
  • The name of a unit dimension, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each - * other is equivalent to multiplication)
  • + *
  • The name of a unit dimension, which multiplies or divides the result + * based on preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
* - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-04-13 * @since v0.2.0 */ - public ObjectProduct getDimensionFromExpression(final String expression) { + public ObjectProduct getDimensionFromExpression( + final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a dimension as an alias first if (this.containsDimensionName(expression)) return this.getDimension(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + return this.unitDimensionParser.parseExpression(modifiedExpression); } - + /** - * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an - * {@code IllegalArgumentException}. + * Gets a unit. If it is linear, cast it to a LinearUnit and return it. + * Otherwise, throw an {@code IllegalArgumentException}. * - * @param name - * unit's name + * @param name unit's name * @return unit * @since 2019-03-22 * @since v0.2.0 */ private LinearUnit getLinearUnit(final String name) { // see if I am using a function-unit like tempC(100) + Objects.requireNonNull(name, "name may not be null"); if (name.contains("(") && name.contains(")")) { // break it into function name and value final List parts = Arrays.asList(name.split("\\(")); if (parts.size() != 2) - throw new IllegalArgumentException("Format nonlinear units like: unit(value)."); - + throw new IllegalArgumentException( + "Format nonlinear units like: unit(value)."); + // solve the function final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1)); + final double value = Double.parseDouble( + parts.get(1).substring(0, parts.get(1).length() - 1)); return LinearUnit.fromUnitValue(unit, value); } else { // get a linear unit @@ -1415,15 +1601,27 @@ public final class UnitDatabase { if (unit instanceof LinearUnit) return (LinearUnit) unit; else - throw new IllegalArgumentException(String.format("%s is not a linear unit.", name)); + throw new IllegalArgumentException( + String.format("%s is not a linear unit.", name)); } } - + + /** + * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be + * converted to their base units. + * + * @param name name of unit + * @return {@code LinearUnitValue} instance + * @since 2020-08-04 + */ + private LinearUnitValue getLinearUnitValue(final String name) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } + /** * Gets a unit prefix from the database from its name * - * @param name - * prefix's name + * @param name prefix's name * @return prefix * @since 2019-01-10 * @since v0.1.0 @@ -1435,53 +1633,45 @@ public final class UnitDatabase { return this.prefixes.get(name); } } - + /** * Gets a unit prefix from a prefix expression *

- * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of - * another prefix + * Currently, prefix expressions are much simpler than unit expressions: They + * are either a number or the name of another prefix *

* - * @param expression - * expression to input + * @param expression expression to input * @return prefix - * @throws IllegalArgumentException - * if expression cannot be parsed - * @throws NullPointerException - * if any argument is null + * @throws IllegalArgumentException if expression cannot be parsed + * @throws NullPointerException if any argument is null * @since 2019-01-14 * @since v0.1.0 */ public UnitPrefix getPrefixFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias first if (this.containsUnitName(expression)) return this.getPrefix(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + return this.prefixExpressionParser.parseExpression(modifiedExpression); } - + /** * Gets a unit from the database from its name, looking for prefixes. * - * @param name - * unit's name + * @param name unit's name * @return unit * @since 2019-01-10 * @since v0.1.0 @@ -1491,98 +1681,113 @@ public final class UnitDatabase { final double value = Double.parseDouble(name); return SI.ONE.times(value); } catch (final NumberFormatException e) { - return this.units.get(name); + final Unit unit = this.units.get(name); + if (unit.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(unit.getPrimaryName().get()); + return unit.withName(NameSymbol.ofNullable(name, + unit.getSymbol().orElse(null), otherNames)); + } else if (!unit.getOtherNames().contains(name)) { + final Set otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(name); + return unit.withName( + NameSymbol.ofNullable(unit.getPrimaryName().orElse(null), + unit.getSymbol().orElse(null), otherNames)); + } else + return unit; } - + } - + /** * Uses the database's unit data to parse an expression into a unit *

* The expression is a series of any of the following: *

    - *
  • The name of a unit, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each - * other is equivalent to multiplication)
  • + *
  • The name of a unit, which multiplies or divides the result based on + * preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
  • A number which is multiplied or divided
  • *
* This method only works with linear units. * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-01-07 * @since v0.1.0 */ public Unit getUnitFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias first if (this.containsUnitName(expression)) return this.getUnit(expression); - + // force operators to have spaces String modifiedExpression = expression; modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); modifiedExpression = modifiedExpression.replaceAll("-", " - "); - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + // the previous operation breaks negative numbers, fix them! // (i.e. -2 becomes - 2) for (int i = 0; i < modifiedExpression.length(); i++) { if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2)))) { + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); } } - + return this.unitExpressionParser.parseExpression(modifiedExpression); } - + /** - * Adds all dimensions from a file, using data from the database to parse them. + * Adds all dimensions from a file, using data from the database to parse + * them. *

- * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated - * by any number of tab characters. + * Each line in the file should consist of a name and an expression (parsed + * by getDimensionFromExpression) separated by any number of tab characters. *

*

* Allowed exceptions: *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • *
* - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null * @since 2019-01-13 * @since v0.1.0 */ public void loadDimensionFile(final File file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try (FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader)) { + // while the reader has lines to read, read a line, then parse it, then + // add it long lineCounter = 0; while (reader.ready()) { this.addDimensionFromLine(reader.readLine(), ++lineCounter); @@ -1593,36 +1798,38 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * Adds all units from a file, using data from the database to parse them. *

- * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by - * any number of tab characters. + * Each line in the file should consist of a name and an expression (parsed + * by getUnitFromExpression) separated by any number of tab characters. *

*

* Allowed exceptions: *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • *
* - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null * @since 2019-01-13 * @since v0.1.0 */ public void loadUnitsFile(final File file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try (FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader)) { + // while the reader has lines to read, read a line, then parse it, then + // add it long lineCounter = 0; while (reader.ready()) { this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); @@ -1633,7 +1840,7 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * @return a map mapping prefix names to prefixes * @since 2019-04-13 @@ -1642,33 +1849,40 @@ public final class UnitDatabase { public Map prefixMap() { return Collections.unmodifiableMap(this.prefixes); } - + /** - * @return a string stating the number of units, prefixes and dimensions in the database + * @return a string stating the number of units, prefixes and dimensions in + * the database */ @Override public String toString() { - return String.format("Unit Database with %d units, %d unit prefixes and %d dimensions", - this.prefixlessUnits.size(), this.prefixes.size(), this.dimensions.size()); + return String.format( + "Unit Database with %d units, %d unit prefixes and %d dimensions", + this.prefixlessUnits.size(), this.prefixes.size(), + this.dimensions.size()); } - + /** * Returns a map mapping unit names to units, including units with prefixes. *

- * The returned map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, - * some operations that only work with finite collections, like converting name/entry sets to arrays, will throw an - * {@code IllegalStateException}. + * The returned map is infinite in size if there is at least one unit and at + * least one prefix. If it is infinite, some operations that only work with + * finite collections, like converting name/entry sets to arrays, will throw + * an {@code IllegalStateException}. *

*

- * Specifically, the operations that will throw an IllegalStateException if the map is infinite in size are: + * Specifically, the operations that will throw an IllegalStateException if + * the map is infinite in size are: *

    *
  • {@code unitMap.entrySet().toArray()} (either overloading)
  • *
  • {@code unitMap.keySet().toArray()} (either overloading)
  • *
*

*

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's {@link PrefixedUnitMap#containsValue - * containsValue} and {@link PrefixedUnitMap#values() values()} methods currently ignore prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's + * {@link PrefixedUnitMap#containsValue containsValue} and + * {@link PrefixedUnitMap#values() values()} methods currently ignore + * prefixes. *

* * @return a map mapping unit names to units, including prefixed names @@ -1676,9 +1890,10 @@ public final class UnitDatabase { * @since v0.2.0 */ public Map unitMap() { - return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map. + return this.units; // PrefixedUnitMap is immutable so I don't need to make + // an unmodifiable map. } - + /** * @return a map mapping unit names to units, ignoring prefixes * @since 2019-04-13 -- cgit v1.2.3 From 618b81da627b55dcb051d889c7faffd91804497a Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Aug 2020 16:42:55 -0500 Subject: Added scientific rounding. --- .../converterGUI/UnitConverterGUI.java | 22 +++++++++++---------- src/org/unitConverter/unit/LinearUnitValue.java | 23 +++++++++++++++++++++- src/org/unitConverter/unit/UnitDatabase.java | 18 ++++++++++++++--- 3 files changed, 49 insertions(+), 14 deletions(-) (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 8c70df4..f7c3479 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.NoSuchElementException; import java.util.Set; import java.util.function.Predicate; @@ -270,14 +271,14 @@ final class UnitConverterGUI { final Unit to; try { from = this.database.evaluateUnitExpression(fromUnitString); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); return; } try { to = this.database.getUnitFromExpression(toUnitString); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); return; @@ -304,9 +305,10 @@ final class UnitConverterGUI { } final LinearUnitValue converted = from2.convertTo(to2); - this.view.setExpressionConverterOutputText( - (useSlash ? "1 / " : "") + String.format("%s = %s", - fromUnitString, this.getRoundedString(converted))); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); + final String toString = this.getRoundedString(converted, false); return; } else { // convert to UnitValue @@ -384,18 +386,19 @@ final class UnitConverterGUI { } /** - * Like {@link LinearUnitValue#toString(boolean)} with parameter - * {@code false}, but obeys this unit converter's rounding settings. + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. * * @since 2020-08-04 */ - private final String getRoundedString(final LinearUnitValue value) { + private final String getRoundedString(final LinearUnitValue value, + boolean showUncertainty) { switch (this.roundingType) { case DECIMAL_PLACES: case SIGNIFICANT_DIGITS: return this.getRoundedString(value.asUnitValue()); case SCIENTIFIC: - return value.toString(false); + return value.toString(showUncertainty); default: throw new AssertionError("Invalid switch condition."); } @@ -992,7 +995,6 @@ final class UnitConverterGUI { final JRadioButton relativePrecision = new JRadioButton( "Scientific Precision"); - relativePrecision.setEnabled(false); relativePrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 7096738..5685a6d 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -367,7 +367,7 @@ public final class LinearUnitValue { final double baseUncertainty = this.unit.convertToBase(this.uncertainty); // get rounded strings - final String valueString, baseValueString, uncertaintyString, + String valueString, baseValueString, uncertaintyString, baseUncertaintyString; if (this.isExact()) { valueString = Double.toString(this.value); @@ -438,6 +438,27 @@ public final class LinearUnitValue { return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); } else { + // truncate excess zeroes + if (valueString.contains(".")) { + while (valueString.endsWith("0")) { + valueString = valueString.substring(0, valueString.length() - 1); + } + if (valueString.endsWith(".")) { + valueString = valueString.substring(0, valueString.length() - 1); + } + } + + if (baseValueString.contains(".")) { + while (baseValueString.endsWith("0")) { + baseValueString = baseValueString.substring(0, + baseValueString.length() - 1); + } + if (baseValueString.endsWith(".")) { + baseValueString = baseValueString.substring(0, + baseValueString.length() - 1); + } + } + if (primaryName.isEmpty() && symbol.isEmpty()) return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 0246630..c5432f7 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.math.BigDecimal; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; @@ -1465,7 +1466,7 @@ public final class UnitDatabase { public LinearUnitValue evaluateUnitExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - // attempt to get a unit as an alias first + // attempt to get a unit as an alias, or a number with precision first if (this.containsUnitName(expression)) return this.getLinearUnitValue(expression); @@ -1598,6 +1599,7 @@ public final class UnitDatabase { } else { // get a linear unit final Unit unit = this.getUnit(name); + if (unit instanceof LinearUnit) return (LinearUnit) unit; else @@ -1615,7 +1617,15 @@ public final class UnitDatabase { * @since 2020-08-04 */ private LinearUnitValue getLinearUnitValue(final String name) { - return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + try { + // try to parse it as a number - otherwise it is not a number! + final BigDecimal number = new BigDecimal(name); + + final double uncertainty = Math.pow(10, -number.scale()); + return LinearUnitValue.of(SI.ONE, number.doubleValue(), uncertainty); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } } /** @@ -1682,7 +1692,9 @@ public final class UnitDatabase { return SI.ONE.times(value); } catch (final NumberFormatException e) { final Unit unit = this.units.get(name); - if (unit.getPrimaryName().isEmpty()) + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) return unit.withName(NameSymbol.ofName(name)); else if (!unit.getPrimaryName().get().equals(name)) { final Set otherNames = new HashSet<>(unit.getOtherNames()); -- cgit v1.2.3 From 5a7e8f6fcb175b238eb1d5481513b35039107a3e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 7 Sep 2020 15:15:20 -0500 Subject: Created an UncertainDouble class for uncertainty operations. --- .settings/org.eclipse.jdt.core.prefs | 100 ++++++ src/org/unitConverter/math/DecimalComparison.java | 207 +++++++---- src/org/unitConverter/math/UncertainDouble.java | 411 ++++++++++++++++++++++ src/org/unitConverter/unit/LinearUnit.java | 302 +++++++++------- src/org/unitConverter/unit/LinearUnitValue.java | 252 +++---------- src/org/unitConverter/unit/UnitDatabase.java | 6 +- src/org/unitConverter/unit/UnitTest.java | 7 +- 7 files changed, 885 insertions(+), 400 deletions(-) create mode 100644 src/org/unitConverter/math/UncertainDouble.java (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index ea7a397..f77f6a1 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,13 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve @@ -6,11 +15,102 @@ org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning +org.eclipse.jdt.core.compiler.problem.fallthroughCase=error +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=info +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=info +org.eclipse.jdt.core.compiler.problem.rawTypeReference=error +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=info +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=info +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=info +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=info org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=info +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=error +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=info +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=info +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=disabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=info +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=info +org.eclipse.jdt.core.compiler.problem.unusedParameter=info +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=info +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=info +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java index 859e8da..0f5b91e 100644 --- a/src/org/unitConverter/math/DecimalComparison.java +++ b/src/org/unitConverter/math/DecimalComparison.java @@ -27,42 +27,45 @@ import java.math.BigDecimal; */ public final class DecimalComparison { /** - * The value used for double comparison. If two double values are within this value multiplied by the larger value, - * they are considered equal. + * The value used for double comparison. If two double values are within this + * value multiplied by the larger value, they are considered equal. * * @since 2019-03-18 * @since v0.2.0 */ public static final double DOUBLE_EPSILON = 1.0e-15; - + /** - * The value used for float comparison. If two float values are within this value multiplied by the larger value, - * they are considered equal. + * The value used for float comparison. If two float values are within this + * value multiplied by the larger value, they are considered equal. * * @since 2019-03-18 * @since v0.2.0 */ public static final float FLOAT_EPSILON = 1.0e-6f; - + /** * Tests for equality of double values using {@link #DOUBLE_EPSILON}. *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon using {@link #equals(double, double, double)} (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon using + * {@link #equals(double, double, double)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test + * @param a first value to test + * @param b second value to test * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 @@ -71,57 +74,61 @@ public final class DecimalComparison { public static final boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); } - + /** * Tests for double equality using a custom epsilon value. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly - * reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final double a, final double b, final double epsilon) { + public static final boolean equals(final double a, final double b, + final double epsilon) { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } - + /** * Tests for equality of float values using {@link #FLOAT_EPSILON}. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon using {@link #equals(float, float, float)} (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon using {@link #equals(float, float, float)} + * (this does not make a violation of transitivity impossible, it just + * significantly reduces the chances of it happening) + *
  4. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test + * @param a first value to test + * @param b second value to test * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 @@ -129,53 +136,121 @@ public final class DecimalComparison { public static final boolean equals(final float a, final float b) { return DecimalComparison.equals(a, b, FLOAT_EPSILON); } - + /** * Tests for float equality using a custom epsilon value. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly - * reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final float a, final float b, final float epsilon) { + public static final boolean equals(final float a, final float b, + final float epsilon) { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } - + + /** + * Tests for equality of {@code UncertainDouble} values using + * {@link #DOUBLE_EPSILON}. + *

+ * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon using + * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not + * make a violation of transitivity impossible, it just significantly reduces + * the chances of it happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @return whether they are equal + * @since 2020-09-07 + * @see #hashCode(double) + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b) { + return DecimalComparison.equals(a.value(), b.value()) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); + } + + /** + * Tests for {@code UncertainDouble} equality using a custom epsilon value. + * + *

+ * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b, final double epsilon) { + return DecimalComparison.equals(a.value(), b.value(), epsilon) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), + epsilon); + } + /** - * Takes the hash code of doubles. Values that are equal according to {@link #equals(double, double)} will have the - * same hash code. + * Takes the hash code of doubles. Values that are equal according to + * {@link #equals(double, double)} will have the same hash code. * - * @param d - * double to hash + * @param d double to hash * @return hash code of double * @since 2019-10-16 */ public static final int hash(final double d) { return Float.hashCode((float) d); } - + // You may NOT get any DecimalComparison instances private DecimalComparison() { throw new AssertionError(); } - + } diff --git a/src/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java new file mode 100644 index 0000000..e948df9 --- /dev/null +++ b/src/org/unitConverter/math/UncertainDouble.java @@ -0,0 +1,411 @@ +/** + * @since 2020-09-07 + */ +package org.unitConverter.math; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A double with an associated uncertainty value. For example, 3.2 ± 0.2. + *

+ * All methods in this class throw a NullPointerException if any of their + * arguments is null. + * + * @since 2020-09-07 + */ +public final class UncertainDouble implements Comparable { + /** + * The exact value 0 + */ + public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); + + /** + * A regular expression that can recognize toString forms + */ + private static final Pattern TO_STRING = Pattern + .compile("([a-zA-Z_0-9\\.\\,]+)" // a number + // optional "± [number]" + + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?"); + + /** + * Parses a string in the form of {@link UncertainDouble#toString(boolean)} + * and returns the corresponding {@code UncertainDouble} instance. + *

+ * This method allows some alternative forms of the string representation, + * such as using "+-" instead of "±". + * + * @param s string to parse + * @return {@code UncertainDouble} instance + * @throws IllegalArgumentException if the string is invalid + * @since 2020-09-07 + */ + public static final UncertainDouble fromString(String s) { + Objects.requireNonNull(s, "s may not be null"); + final Matcher matcher = TO_STRING.matcher(s); + + double value, uncertainty; + try { + value = Double.parseDouble(matcher.group(1)); + } catch (IllegalStateException | NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + + final String uncertaintyString = matcher.group(2); + if (uncertaintyString == null) { + uncertainty = 0; + } else { + try { + uncertainty = Double.parseDouble(uncertaintyString); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + } + + return UncertainDouble.of(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and absolute + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble of(double value, double uncertainty) { + return new UncertainDouble(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and relative + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble ofRelative(double value, + double relativeUncertainty) { + return new UncertainDouble(value, value * relativeUncertainty); + } + + private final double value; + + private final double uncertainty; + + /** + * @param value + * @param uncertainty + * @since 2020-09-07 + */ + private UncertainDouble(double value, double uncertainty) { + this.value = value; + // uncertainty should only ever be positive + this.uncertainty = Math.abs(uncertainty); + } + + /** + * Compares this {@code UncertainDouble} with another + * {@code UncertainDouble}. + *

+ * This method only compares the values, not the uncertainties. So 3.1 ± 0.5 + * is considered less than 3.2 ± 0.5, even though they are equivalent. + *

+ * Note: The natural ordering of this class is inconsistent with + * equals. Specifically, if two {@code UncertainDouble} instances {@code a} + * and {@code b} have the same value but different uncertainties, + * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return + * {@code false}. + */ + @Override + public final int compareTo(UncertainDouble o) { + return Double.compare(this.value, o.value); + } + + /** + * Returns the quotient of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedBy(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value / other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the quotient of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedByExact(double other) { + return UncertainDouble.of(this.value / other, this.uncertainty / other); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UncertainDouble)) + return false; + final UncertainDouble other = (UncertainDouble) obj; + if (Double.doubleToLongBits(this.uncertainty) != Double + .doubleToLongBits(other.uncertainty)) + return false; + if (Double.doubleToLongBits(this.value) != Double + .doubleToLongBits(other.value)) + return false; + return true; + } + + /** + * @param other another {@code UncertainDouble} + * @return true iff this and {@code other} are within each other's + * uncertainty range. + * @since 2020-09-07 + */ + public final boolean equivalent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return Math.abs(this.value - other.value) <= Math.min(this.uncertainty, + other.uncertainty); + } + + /** + * Gets the preferred scale for rounding a value for toString. + * + * @since 2020-09-07 + */ + private final int getDisplayScale() { + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the + // uncertainty. + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // the scale that will give the uncertainty two decimal places + final int twoDecimalPlacesScale = bigUncertainty.scale() + - bigUncertainty.precision() + 2; + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); + + if (roundedUncertainty.unscaledValue().intValue() >= 20) + return twoDecimalPlacesScale - 1; // one decimal place + else + return twoDecimalPlacesScale; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(this.uncertainty); + result = prime * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.value); + result = prime * result + (int) (temp ^ temp >>> 32); + return result; + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-09-07 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value - other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the difference of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minusExact(double other) { + return UncertainDouble.of(this.value - other, this.uncertainty); + } + + /** + * Returns the sum of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value + other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the sum of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plusExact(double other) { + return UncertainDouble.of(this.value + other, this.uncertainty); + } + + /** + * @return relative uncertainty + * @since 2020-09-07 + */ + public final double relativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * Returns the product of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble times(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value * other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the product of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble timesExact(double other) { + return UncertainDouble.of(this.value * other, this.uncertainty * other); + } + + /** + * Returns the result of {@code this} raised to the exponent {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + + final double result = Math.pow(this.value, other.value); + final double relativeUncertainty = Math.hypot( + other.value * this.relativeUncertainty(), + Math.log(this.value) * other.uncertainty); + + return UncertainDouble.ofRelative(result, relativeUncertainty); + } + + /** + * Returns the result of {@code this} raised the exact exponent + * {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponentExact(double other) { + return UncertainDouble.ofRelative(Math.pow(this.value, other), + this.relativeUncertainty() * other); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * This method returns the same value as {@link #toString(boolean)}, but + * {@code showUncertainty} is true if and only if the uncertainty is + * non-zero. + * + *

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString() = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString() = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString() = "-5.01"
+	 * 
+ * + * @since 2020-09-07 + */ + @Override + public final String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * If {@code showUncertainty} is true, the string will be of the form "VALUE + * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE" + *

+ * VALUE represents a string representation of this {@code UncertainDouble}'s + * value. If the uncertainty is non-zero, the string will be rounded to the + * same precision as the uncertainty, otherwise it will not be rounded. The + * string is still rounded if {@code showUncertainty} is false.
+ * UNCERTAINTY represents a string representation of this + * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X + * (where X represents any digit) it will be rounded to two significant + * digits otherwise it will be rounded to one significant digit. + *

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
+	 * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
+	 * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
+	 * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
+	 * 
+ * + * @since 2020-09-07 + */ + public final String toString(boolean showUncertainty) { + String valueString, uncertaintyString; + + // generate the string representation of value and uncertainty + if (this.isExact()) { + uncertaintyString = "0.0"; + valueString = Double.toString(this.value); + + } else { + // round the value and uncertainty according to getDisplayScale() + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + final int displayScale = this.getDisplayScale(); + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(displayScale, RoundingMode.HALF_EVEN); + final BigDecimal roundedValue = bigValue.setScale(displayScale, + RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + } + + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); + } + + /** + * @return absolute uncertainty + * @since 2020-09-07 + */ + public final double uncertainty() { + return this.uncertainty; + } + + /** + * @return value without uncertainty + * @since 2020-09-07 + */ + public final double value() { + return this.value; + } +} diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 762572a..2d63ca7 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -20,89 +20,83 @@ import java.util.Objects; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** - * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds. + * A unit that can be expressed as a product of its base and a number. For + * example, kilometres, inches and pounds. * * @author Adrien Hopkins * @since 2019-10-16 */ public final class LinearUnit extends Unit { /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' * - * @param unit - * unit to convert - * @param value - * value to convert + * @param unit unit to convert + * @param value value to convert * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 - * @throws NullPointerException - * if unit is null + * @throws NullPointerException if unit is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), NameSymbol.EMPTY); } - + /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' * - * @param unit - * unit to convert - * @param value - * value to convert - * @param ns - * name(s) and symbol of unit + * @param unit unit to convert + * @param value value to convert + * @param ns name(s) and symbol of unit * @return value expressed as a {@code LinearUnit} * @since 2019-10-21 - * @throws NullPointerException - * if unit or ns is null + * @throws NullPointerException if unit or ns is null */ - public static LinearUnit fromUnitValue(final Unit unit, final double value, final NameSymbol ns) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), ns); } - + /** - * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of - * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by * @return product of base and conversion factor * @since 2019-10-16 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor) { + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor) { return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); } - + /** - * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of - * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by - * @param ns - * name(s) and symbol of unit + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @param ns name(s) and symbol of unit * @return product of base and conversion factor * @since 2019-10-21 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor, - final NameSymbol ns) { + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { return new LinearUnit(unitBase, conversionFactor, ns); } - + /** * The value of this unit as represented in its base form. Mathematically, * @@ -113,21 +107,20 @@ public final class LinearUnit extends Unit { * @since 2019-10-16 */ private final double conversionFactor; - + /** * Creates the {@code LinearUnit}. * - * @param unitBase - * base of linear unit - * @param conversionFactor - * conversion factor between base and unit + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit * @since 2019-10-16 */ - private LinearUnit(final ObjectProduct unitBase, final double conversionFactor, final NameSymbol ns) { + private LinearUnit(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { super(unitBase, ns); this.conversionFactor = conversionFactor; } - + /** * {@inheritDoc} * @@ -137,7 +130,32 @@ public final class LinearUnit extends Unit { protected double convertFromBase(final double value) { return value / this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} value expressed in this unit to an + * {@code UncertainValue} value expressed in {@code other}. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + if (this.canConvertTo(other)) + return value.timesExact( + this.getConversionFactor() / other.getConversionFactor()); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + + } + /** * {@inheritDoc} * @@ -147,12 +165,20 @@ public final class LinearUnit extends Unit { protected double convertToBase(final double value) { return value * this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + /** * Divides this unit by a scalar. * - * @param divisor - * scalar to divide by + * @param divisor scalar to divide by * @return quotient * @since 2018-12-23 * @since v0.1.0 @@ -160,26 +186,26 @@ public final class LinearUnit extends Unit { public LinearUnit dividedBy(final double divisor) { return valueOf(this.getBase(), this.getConversionFactor() / divisor); } - + /** * Returns the quotient of this unit and another. * - * @param divisor - * unit to divide by + * @param divisor unit to divide by * @return quotient of two units - * @throws NullPointerException - * if {@code divisor} is null + * @throws NullPointerException if {@code divisor} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit dividedBy(final LinearUnit divisor) { Objects.requireNonNull(divisor, "other must not be null"); - + // divide the units - final ObjectProduct base = this.getBase().dividedBy(divisor.getBase()); - return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); + final ObjectProduct base = this.getBase() + .dividedBy(divisor.getBase()); + return valueOf(base, + this.getConversionFactor() / divisor.getConversionFactor()); } - + /** * {@inheritDoc} * @@ -191,9 +217,10 @@ public final class LinearUnit extends Unit { return false; final LinearUnit other = (LinearUnit) obj; return Objects.equals(this.getBase(), other.getBase()) - && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); } - + /** * @return conversion factor * @since 2019-10-16 @@ -201,7 +228,7 @@ public final class LinearUnit extends Unit { public double getConversionFactor() { return this.conversionFactor; } - + /** * {@inheritDoc} * @@ -209,18 +236,20 @@ public final class LinearUnit extends Unit { */ @Override public int hashCode() { - return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); + return 31 * this.getBase().hashCode() + + DecimalComparison.hash(this.getConversionFactor()); } - + /** - * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there is a {@code BaseUnit b} where + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where * {@code b.asLinearUnit().equals(this)} returns {@code true}.) * @since 2019-10-16 */ public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); } - + /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 @@ -228,70 +257,73 @@ public final class LinearUnit extends Unit { public boolean isCoherent() { return this.getConversionFactor() == 1; } - + /** * Returns the difference of this unit and another. *

- * Two units can be subtracted if they have the same base. Note that {@link #canConvertTo} can be used to determine - * this. If {@code subtrahend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be subtracted if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code subtrahend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. *

* - * @param subtrahend - * unit to subtract + * @param subtrahend unit to subtract * @return difference of units - * @throws IllegalArgumentException - * if {@code subtrahend} is not compatible for subtraction as described above - * @throws NullPointerException - * if {@code subtrahend} is null + * @throws IllegalArgumentException if {@code subtrahend} is not compatible + * for subtraction as described above + * @throws NullPointerException if {@code subtrahend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit minus(final LinearUnit 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)); - + 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()); + return valueOf(this.getBase(), + this.getConversionFactor() - subtrahend.getConversionFactor()); } - + /** * Returns the sum of this unit and another. *

- * Two units can be added if they have the same base. Note that {@link #canConvertTo} can be used to determine this. - * If {@code addend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be added if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code addend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. *

* - * @param addend - * unit to add + * @param addend unit to add * @return sum of units - * @throws IllegalArgumentException - * if {@code addend} is not compatible for addition as described above - * @throws NullPointerException - * if {@code addend} is null + * @throws IllegalArgumentException if {@code addend} is not compatible for + * addition as described above + * @throws NullPointerException if {@code addend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit plus(final LinearUnit addend) { Objects.requireNonNull(addend, "addend must not be null."); - + // reject addends that cannot be added to this unit if (!this.getBase().equals(addend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this, + addend)); + // add the units - return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() + addend.getConversionFactor()); } - + /** * Multiplies this unit by a scalar. * - * @param multiplier - * scalar to multiply by + * @param multiplier scalar to multiply by * @return product * @since 2018-12-23 * @since v0.1.0 @@ -299,39 +331,39 @@ public final class LinearUnit extends Unit { public LinearUnit times(final double multiplier) { return valueOf(this.getBase(), this.getConversionFactor() * multiplier); } - + /** * Returns the product of this unit and another. * - * @param multiplier - * unit to multiply by + * @param multiplier unit to multiply by * @return product of two units - * @throws NullPointerException - * if {@code multiplier} is null + * @throws NullPointerException if {@code multiplier} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit times(final LinearUnit multiplier) { Objects.requireNonNull(multiplier, "other must not be null"); - + // multiply the units - final ObjectProduct base = this.getBase().times(multiplier.getBase()); - return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); + final ObjectProduct base = this.getBase() + .times(multiplier.getBase()); + return valueOf(base, + this.getConversionFactor() * multiplier.getConversionFactor()); } - + /** * Returns this unit but to an exponent. * - * @param exponent - * exponent to exponentiate unit to + * @param exponent exponent to exponentiate unit to * @return exponentiated unit * @since 2019-01-15 * @since v0.1.0 */ public LinearUnit toExponent(final int exponent) { - return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); + return valueOf(this.getBase().toExponent(exponent), + Math.pow(this.conversionFactor, exponent)); } - + /** * @return a string providing a definition of this unit * @since 2019-10-21 @@ -339,10 +371,13 @@ public final class LinearUnit extends Unit { @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "") + ", " - + Double.toString(this.conversionFactor) + " * " + this.getBase().toString(u -> u.getSymbol().get()); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", " + Double.toString(this.conversionFactor) + " * " + + this.getBase().toString(u -> u.getSymbol().get()); } - + @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); @@ -351,37 +386,38 @@ public final class LinearUnit extends Unit { /** * Returns the result of applying {@code prefix} to this unit. *

- * If this unit and the provided prefix have a primary name, the returned unit will have a primary name (prefix's - * name + unit's name).
- * If this unit and the provided prefix have a symbol, the returned unit will have a symbol.
- * This method ignores alternate names of both this unit and the provided prefix. + * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name).
+ * If this unit and the provided prefix have a symbol, the returned unit will + * have a symbol.
+ * This method ignores alternate names of both this unit and the provided + * prefix. * - * @param prefix - * prefix to apply + * @param prefix prefix to apply * @return unit with prefix * @since 2019-03-18 * @since v0.2.0 - * @throws NullPointerException - * if prefix is null + * @throws NullPointerException if prefix is null */ public LinearUnit withPrefix(final UnitPrefix prefix) { final LinearUnit unit = this.times(prefix.getMultiplier()); - + // create new name and symbol, if possible final String name; - if (this.getPrimaryName().isPresent() && prefix.getPrimaryName().isPresent()) { + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); } else { name = null; } - + final String symbol; if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { symbol = prefix.getSymbol().get() + this.getSymbol().get(); } else { symbol = null; } - + return unit.withName(NameSymbol.ofNullable(name, symbol)); } } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 5685a6d..d86d344 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -3,12 +3,11 @@ */ package org.unitConverter.unit; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; /** * A possibly uncertain value expressed in a linear unit. @@ -33,7 +32,8 @@ public final class LinearUnitValue { public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { return new LinearUnitValue( - Objects.requireNonNull(unit, "unit must not be null"), value, 0); + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); } /** @@ -46,41 +46,23 @@ public final class LinearUnitValue { * @since 2020-07-26 */ public static final LinearUnitValue of(final LinearUnit unit, - final double value, final double uncertainty) { + final UncertainDouble value) { return new LinearUnitValue( - Objects.requireNonNull(unit, "unit must not be null"), value, - uncertainty); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param relativeUncertainty relative uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-28 - */ - public static final LinearUnitValue ofRelative(final LinearUnit unit, - final double value, final double relativeUncertainty) { - return LinearUnitValue.of(unit, value, relativeUncertainty * value); + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); } private final LinearUnit unit; - private final double value; - private final double uncertainty; + private final UncertainDouble value; /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value + * @param unit unit to express as + * @param value value to express * @since 2020-07-26 */ - private LinearUnitValue(final LinearUnit unit, final double value, - final double uncertainty) { + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { this.unit = unit; this.value = value; - this.uncertainty = uncertainty; } /** @@ -89,7 +71,7 @@ public final class LinearUnitValue { * @since 2020-08-04 */ public final UnitValue asUnitValue() { - return UnitValue.of(this.unit, this.value); + return UnitValue.of(this.unit, this.value.value()); } /** @@ -110,8 +92,7 @@ public final class LinearUnitValue { * @since 2020-07-26 */ public final LinearUnitValue convertTo(final LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), - this.unit.convertTo(other, this.uncertainty)); + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); } /** @@ -122,8 +103,7 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue dividedBy(final double divisor) { - return LinearUnitValue.of(this.unit, this.value / divisor, - this.uncertainty / divisor); + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); } /** @@ -134,10 +114,8 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue dividedBy(final LinearUnitValue divisor) { - return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), - this.value / divisor.value, - Math.hypot(this.getRelativeUncertainty(), - divisor.getRelativeUncertainty())); + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); } /** @@ -154,12 +132,8 @@ public final class LinearUnitValue { return false; final LinearUnitValue other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits( - this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits( - other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); } /** @@ -180,9 +154,7 @@ public final class LinearUnitValue { final LinearUnitValue other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && DecimalComparison.equals(this.unit.convertToBase(this.value), - other.unit.convertToBase(other.value)) - && DecimalComparison.equals(this.getRelativeUncertainty(), - other.getRelativeUncertainty()); + other.unit.convertToBase(other.value)); } /** @@ -195,32 +167,11 @@ public final class LinearUnitValue { if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) return false; - final double thisBaseValue = this.unit.convertToBase(this.value); - final double otherBaseValue = other.unit.convertToBase(other.value); - final double thisBaseUncertainty = this.unit - .convertToBase(this.uncertainty); - final double otherBaseUncertainty = other.unit - .convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math - .min(thisBaseUncertainty, otherBaseUncertainty); - } - - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return this.uncertainty / this.value; - } - - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return this.uncertainty; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); } /** @@ -237,24 +188,22 @@ public final class LinearUnitValue { * * @since 2020-07-26 */ - public final double getValue() { + public final UncertainDouble getValue() { return this.value; } + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + @Override public int hashCode() { return Objects.hash(this.unit.getBase(), - this.unit.convertToBase(this.getValue()), - this.getRelativeUncertainty()); - } - - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return this.uncertainty == 0; + this.unit.convertToBase(this.getValue())); } /** @@ -276,8 +225,8 @@ public final class LinearUnitValue { this.unit, subtrahend.unit)); final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); + return LinearUnitValue.of(this.unit, + this.value.minus(otherConverted.value)); } /** @@ -298,8 +247,8 @@ public final class LinearUnitValue { addend.unit)); final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); } /** @@ -310,8 +259,7 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue times(final double multiplier) { - return LinearUnitValue.of(this.unit, this.value * multiplier, - this.uncertainty * multiplier); + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); } /** @@ -322,10 +270,8 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue times(final LinearUnitValue multiplier) { - return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), - this.value * multiplier.value, - Math.hypot(this.getRelativeUncertainty(), - multiplier.getRelativeUncertainty())); + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); } /** @@ -336,14 +282,13 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue toExponent(final int exponent) { - return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), - Math.pow(this.value, exponent), - this.getRelativeUncertainty() * Math.sqrt(exponent)); + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); } @Override public String toString() { - return this.toString(!this.isExact()); + return this.toString(!this.value.isExact()); } /** @@ -363,107 +308,22 @@ public final class LinearUnitValue { final Optional symbol = this.unit.getSymbol(); final String chosenName = symbol.orElse(primaryName.orElse(null)); - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + final UncertainDouble baseValue = this.unit.convertToBase(this.value); // get rounded strings - String valueString, baseValueString, uncertaintyString, - baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(this.value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal - // point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the - // uncertainty. - BigDecimal roundedUncertainty = bigUncertainty.setScale( - bigUncertainty.scale() - bigUncertainty.precision() + 2, - RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale( - bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue - .setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal - .valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty - .setScale( - bigBaseUncertainty.scale() - - bigBaseUncertainty.precision() + 2, - RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty - .setScale( - bigBaseUncertainty.scale() - - bigBaseUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale( - roundedBaseUncertainty.scale(), RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } - } + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", - valueString, uncertaintyString, baseValueString, - baseUncertaintyString, this.unit.getBase()); - else - return String.format("(%s ± %s) %s", valueString, uncertaintyString, - chosenName); - } else { - // truncate excess zeroes - if (valueString.contains(".")) { - while (valueString.endsWith("0")) { - valueString = valueString.substring(0, valueString.length() - 1); - } - if (valueString.endsWith(".")) { - valueString = valueString.substring(0, valueString.length() - 1); - } - } - - if (baseValueString.contains(".")) { - while (baseValueString.endsWith("0")) { - baseValueString = baseValueString.substring(0, - baseValueString.length() - 1); - } - if (baseValueString.endsWith(".")) { - baseValueString = baseValueString.substring(0, - baseValueString.length() - 1); - } - } - - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("%s unnamed unit (= %s %s)", valueString, - baseValueString, this.unit.getBase()); - else - return String.format("%s %s", valueString, chosenName); - } + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 9812bd0..9ca9617 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -46,6 +46,7 @@ import org.unitConverter.math.ConditionalExistenceCollections; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ExpressionParser; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. @@ -1134,7 +1135,7 @@ public final class UnitDatabase { // exponent function - first check if o2 is a number, if (exponentValue.canConvertTo(SI.ONE)) { // then check if it is an integer, - final double exponent = exponentValue.getValue(); + final double exponent = exponentValue.getValueExact(); if (DecimalComparison.equals(exponent % 1, 0)) // then exponentiate return base.toExponent((int) (exponent + 0.5)); @@ -1650,7 +1651,8 @@ public final class UnitDatabase { final BigDecimal number = new BigDecimal(name); final double uncertainty = Math.pow(10, -number.scale()); - return LinearUnitValue.of(SI.ONE, number.doubleValue(), uncertainty); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); } catch (final NumberFormatException e) { return LinearUnitValue.getExact(this.getLinearUnit(name), 1); } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index ff83805..c0711dc 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -56,9 +56,10 @@ class UnitTest { final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); // make sure addition is done correctly - assertEquals(51.576, value1.plus(value2).getValue(), 0.001); - assertEquals(15.5, value1.plus(value3).getValue()); - assertEquals(52.076, value1.plus(value2).plus(value3).getValue(), 0.001); + assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001); + assertEquals(15.5, value1.plus(value3).getValueExact()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValueExact(), + 0.001); // make sure addition uses the correct unit, and is still associative // (ignoring floating-point rounding errors) -- cgit v1.2.3 From cd33f886dfbd35c0ee3d8cf5b553ea3481b0b3a1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 2 Oct 2020 10:16:10 -0500 Subject: Added Unitlike objects --- src/org/unitConverter/math/UncertainDouble.java | 15 +- src/org/unitConverter/unit/FunctionalUnitlike.java | 72 ++++++ src/org/unitConverter/unit/LinearUnit.java | 18 ++ src/org/unitConverter/unit/LinearUnitValue.java | 22 +- src/org/unitConverter/unit/Nameable.java | 59 +++++ src/org/unitConverter/unit/Unit.java | 118 ++++++---- src/org/unitConverter/unit/UnitValue.java | 149 ++++++++---- src/org/unitConverter/unit/Unitlike.java | 260 +++++++++++++++++++++ src/org/unitConverter/unit/UnitlikeValue.java | 172 ++++++++++++++ 9 files changed, 785 insertions(+), 100 deletions(-) create mode 100644 src/org/unitConverter/unit/FunctionalUnitlike.java create mode 100644 src/org/unitConverter/unit/Nameable.java create mode 100644 src/org/unitConverter/unit/Unitlike.java create mode 100644 src/org/unitConverter/unit/UnitlikeValue.java (limited to 'src/org/unitConverter/unit/LinearUnitValue.java') diff --git a/src/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java index e948df9..9601c75 100644 --- a/src/org/unitConverter/math/UncertainDouble.java +++ b/src/org/unitConverter/math/UncertainDouble.java @@ -1,5 +1,18 @@ /** - * @since 2020-09-07 + * 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 . */ package org.unitConverter.math; diff --git a/src/org/unitConverter/unit/FunctionalUnitlike.java b/src/org/unitConverter/unit/FunctionalUnitlike.java new file mode 100644 index 0000000..21c1fca --- /dev/null +++ b/src/org/unitConverter/unit/FunctionalUnitlike.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unitlike form that converts using two conversion functions. + * + * @since 2020-09-07 + */ +final class FunctionalUnitlike extends Unitlike { + /** + * 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 converterFrom; + + /** + * A function that accepts a value in the unitlike form and returns a value + * in the unitlike form's base. + */ + private final ToDoubleFunction 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 unitBase, NameSymbol ns, + DoubleFunction converterFrom, ToDoubleFunction converterTo) { + super(unitBase, ns); + this.converterFrom = converterFrom; + this.converterTo = converterTo; + } + + @Override + protected V convertFromBase(double value) { + return this.converterFrom.apply(value); + } + + @Override + protected double convertToBase(V value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 2d63ca7..b7f33d5 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -64,6 +64,24 @@ public final class LinearUnit extends Unit { unit.convertToBase(value), ns); } + /** + * @return the base unit associated with {@code unit}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unit unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * @return the base unit associated with {@code unitlike}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unitlike unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + /** * Gets a {@code LinearUnit} from a unit base and a conversion factor. In * other words, gets the product of {@code unitBase} and diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index d86d344..8de734e 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -1,5 +1,18 @@ /** - * + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . */ package org.unitConverter.unit; @@ -53,6 +66,7 @@ public final class LinearUnitValue { } private final LinearUnit unit; + private final UncertainDouble value; /** @@ -176,8 +190,7 @@ public final class LinearUnitValue { /** * @return the unit - * - * @since 2020-07-26 + * @since 2020-09-29 */ public final LinearUnit getUnit() { return this.unit; @@ -185,8 +198,7 @@ public final class LinearUnitValue { /** * @return the value - * - * @since 2020-07-26 + * @since 2020-09-29 */ public final UncertainDouble getValue() { return this.value; diff --git a/src/org/unitConverter/unit/Nameable.java b/src/org/unitConverter/unit/Nameable.java new file mode 100644 index 0000000..36740ab --- /dev/null +++ b/src/org/unitConverter/unit/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index eb9b000..0a3298f 100644 --- a/src/org/unitConverter/unit/Unit.java +++ b/src/org/unitConverter/unit/Unit.java @@ -16,12 +16,10 @@ */ package org.unitConverter.unit; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.DoubleUnaryOperator; @@ -34,7 +32,7 @@ import org.unitConverter.math.ObjectProduct; * @author Adrien Hopkins * @since 2019-10-16 */ -public abstract class Unit { +public abstract class Unit implements Nameable { /** * Returns a unit from its base and the functions it uses to convert to and * from its base. @@ -98,19 +96,11 @@ public abstract class Unit { private final ObjectProduct unitBase; /** - * The primary name used by this unit. - */ - private final Optional primaryName; - - /** - * A short symbol used to represent this unit. - */ - private final Optional symbol; - - /** - * A set of any additional names and/or spellings that the unit uses. + * This unit's name(s) and symbol + * + * @since 2020-09-07 */ - private final Set otherNames; + private final NameSymbol nameSymbol; /** * Cache storing the result of getDimension() @@ -120,20 +110,17 @@ public abstract class Unit { private transient ObjectProduct dimension = null; /** - * Creates the {@code AbstractUnit}. + * Creates the {@code Unit}. * * @param unitBase base of unit * @param ns names and symbol of unit * @since 2019-10-16 * @throws NullPointerException if unitBase or ns is null */ - protected Unit(final ObjectProduct unitBase, final NameSymbol ns) { + Unit(ObjectProduct unitBase, NameSymbol ns) { this.unitBase = Objects.requireNonNull(unitBase, - "unitBase must not be null."); - this.primaryName = Objects.requireNonNull(ns, "ns must not be null.") - .getPrimaryName(); - this.symbol = ns.getSymbol(); - this.otherNames = ns.getOtherNames(); + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); } /** @@ -147,18 +134,25 @@ public abstract class Unit { this.unitBase = ObjectProduct.oneOf((BaseUnit) this); } else throw new AssertionError(); - this.primaryName = Optional.of(primaryName); - this.symbol = Optional.of(symbol); - this.otherNames = Collections.unmodifiableSet(new HashSet<>(Objects - .requireNonNull(otherNames, "additionalNames must not be null."))); + this.nameSymbol = NameSymbol.of(primaryName, symbol, + new HashSet<>(otherNames)); + } + + /** + * @return this unit as a {@link Unitlike} + * @since 2020-09-07 + */ + public final Unitlike asUnitlike() { + return Unitlike.fromConversionFunctions(this.getBase(), + this::convertFromBase, this::convertToBase, this.getNameSymbol()); } /** * Checks if a value expressed in this unit can be converted to a value * expressed in {@code other} * - * @param other unit to test with - * @return true if the units are compatible + * @param other unit or unitlike form to test with + * @return true if they are compatible * @since 2019-01-13 * @since v0.1.0 * @throws NullPointerException if other is null @@ -168,6 +162,21 @@ public abstract class Unit { return Objects.equals(this.getBase(), other.getBase()); } + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unitlike 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. @@ -218,6 +227,34 @@ public abstract class Unit { 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 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 convertTo(final Unitlike other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + /** * Converts from a value expressed in this unit to a value expressed in this * unit's base unit. @@ -270,27 +307,12 @@ public abstract class Unit { } /** - * @return additionalNames - * @since 2019-10-21 - */ - public final Set getOtherNames() { - return this.otherNames; - } - - /** - * @return primaryName - * @since 2019-10-21 + * @return the nameSymbol + * @since 2020-09-07 */ - public final Optional getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-10-21 - */ - public final Optional getSymbol() { - return this.symbol; + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; } /** diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java index 9e565d9..8932ccc 100644 --- a/src/org/unitConverter/unit/UnitValue.java +++ b/src/org/unitConverter/unit/UnitValue.java @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ package org.unitConverter.unit; import java.util.Objects; @@ -14,60 +30,57 @@ import java.util.Optional; */ public final class UnitValue { /** + * Creates a {@code UnitValue} from a unit and the associated value. + * * @param unit unit to use * @param value value to use * @return {@code UnitValue} instance */ public static UnitValue of(Unit unit, double value) { - return new UnitValue(Objects.requireNonNull(unit, "unit must not be null"), value); + return new UnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value); } - + private final Unit unit; private final double value; - + /** * @param unit the unit being used * @param value the value being represented */ - private UnitValue(Unit unit, double value) { + private UnitValue(Unit unit, Double value) { this.unit = unit; this.value = value; } - + /** - * @return the unit + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 */ - public final Unit getUnit() { - return unit; + public final boolean canConvertTo(Unit other) { + return this.unit.canConvertTo(other); } - + /** - * @return the value + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 */ - public final double getValue() { - return value; + public final boolean canConvertTo(Unitlike other) { + return this.unit.canConvertTo(other); } - + /** - * Converts this {@code UnitValue} into an equivalent {@code LinearUnitValue} by - * using this unit's base unit. + * Returns a UnitValue that represents the same value expressed in a + * different unit * - * @param newName A new name for the base unit. Use {@link NameSymbol#EMPTY} if - * you don't want one. - */ - public final LinearUnitValue asLinearUnitValue(NameSymbol newName) { - LinearUnit base = LinearUnit.valueOf(unit.getBase(), 1, newName); - return LinearUnitValue.getExact(base, base.convertToBase(value)); - } - - /** - * @param other a {@code Unit} - * @return true iff this value can be represented with {@code other}. + * @param other new unit to express value in + * @return value expressed in {@code other} */ - public final boolean canConvertTo(Unit other) { - return this.unit.canConvertTo(other); + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.getUnit().convertTo(other, this.getValue())); } - + /** * Returns a UnitValue that represents the same value expressed in a * different unit @@ -75,39 +88,83 @@ public final class UnitValue { * @param other new unit to express value in * @return value expressed in {@code other} */ - public final UnitValue convertTo(Unit other) { - return UnitValue.of(other, this.getUnit().convertTo(other, this.getValue())); + public final UnitlikeValue convertTo(Unitlike other) { + return UnitlikeValue.of(other, + this.getUnit().convertTo(other, this.getValue())); } - + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnit().convertTo(other, this.getValue())); + } + /** - * Returns true if this and obj represent the same value, regardless of whether - * or not they are expressed in the same unit. So (1000 m).equals(1 km) returns - * true. + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. */ @Override public boolean equals(Object obj) { if (!(obj instanceof UnitValue)) return false; final UnitValue other = (UnitValue) obj; - return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits(other.unit.convertToBase(other.getValue())); + return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) + && Double.doubleToLongBits( + this.getUnit().convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.getUnit().convertToBase(other.getValue())); } - + + /** + * @return the unit + * @since 2020-09-29 + */ + public final Unit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final double getValue() { + return this.value; + } + @Override public int hashCode() { - return Objects.hash(this.unit.getBase(), this.unit.convertFromBase(this.getValue())); + return Objects.hash(this.getUnit().getBase(), + this.getUnit().convertFromBase(this.getValue())); } - + @Override public String toString() { - Optional primaryName = this.getUnit().getPrimaryName(); - Optional symbol = this.getUnit().getSymbol(); + final Optional primaryName = this.getUnit().getPrimaryName(); + final Optional symbol = this.getUnit().getSymbol(); if (primaryName.isEmpty() && symbol.isEmpty()) { - double baseValue = this.getUnit().convertToBase(this.getValue()); - return String.format("%s unnamed unit (= %s %s)", this.getValue(), baseValue, this.getUnit().getBase()); + final double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnit().getBase()); } else { - String unitName = symbol.orElse(primaryName.get()); + final String unitName = symbol.orElse(primaryName.get()); return this.getValue() + " " + unitName; } } diff --git a/src/org/unitConverter/unit/Unitlike.java b/src/org/unitConverter/unit/Unitlike.java new file mode 100644 index 0000000..a6ddb04 --- /dev/null +++ b/src/org/unitConverter/unit/Unitlike.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * An object that can convert a value between multiple forms (instances of the + * object); like a unit but the "converted value" can be any type. + * + * @since 2020-09-07 + */ +public abstract class Unitlike 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 Unitlike fromConversionFunctions( + final ObjectProduct base, + final DoubleFunction converterFrom, + final ToDoubleFunction 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 Unitlike fromConversionFunctions( + final ObjectProduct base, + final DoubleFunction converterFrom, + final ToDoubleFunction 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 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 dimension = null; + + /** + * @param unitBase + * @since 2020-09-07 + */ + Unitlike(ObjectProduct 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 boolean canConvertTo(final Unitlike 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 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 convertTo(final Unitlike 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 getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map 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 withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/org/unitConverter/unit/UnitlikeValue.java b/src/org/unitConverter/unit/UnitlikeValue.java new file mode 100644 index 0000000..669a123 --- /dev/null +++ b/src/org/unitConverter/unit/UnitlikeValue.java @@ -0,0 +1,172 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Optional; + +/** + * + * @since 2020-09-07 + */ +final class UnitlikeValue { + /** + * Gets a {@code UnitlikeValue}. + * + * @since 2020-10-02 + */ + public static UnitlikeValue of(Unitlike unitlike, V value) { + return new UnitlikeValue<>(unitlike, value); + } + + private final Unitlike unitlike; + private final V value; + + /** + * @param unitlike + * @param value + * @since 2020-09-07 + */ + private UnitlikeValue(Unitlike 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 boolean canConvertTo(Unitlike other) { + return this.unitlike.canConvertTo(other); + } + + /** + * 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 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 UnitlikeValue convertTo(Unitlike other) { + return UnitlikeValue.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 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 primaryName = this.getUnitlike().getPrimaryName(); + final Optional 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; + } + } +} -- cgit v1.2.3