diff options
-rw-r--r-- | src/org/unitConverter/unit/LinearUnit.java | 12 | ||||
-rw-r--r-- | src/org/unitConverter/unit/LinearUnitValue.java | 300 | ||||
-rw-r--r-- | src/org/unitConverter/unit/UnitValue.java | 114 |
3 files changed, 420 insertions, 6 deletions
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 1e5ae53..762572a 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -246,16 +246,16 @@ public final class LinearUnit extends Unit { * @since 2019-03-17 * @since v0.2.0 */ - public LinearUnit minus(final LinearUnit subtrahendend) { - Objects.requireNonNull(subtrahendend, "addend must not be null."); + public LinearUnit minus(final LinearUnit subtrahend) { + Objects.requireNonNull(subtrahend, "addend must not be null."); // reject subtrahends that cannot be added to this unit - if (!this.getBase().equals(subtrahendend.getBase())) + if (!this.getBase().equals(subtrahend.getBase())) throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahend)); // subtract the units - return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); + return valueOf(this.getBase(), this.getConversionFactor() - subtrahend.getConversionFactor()); } /** @@ -347,7 +347,7 @@ public final class LinearUnit extends Unit { public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); } - + /** * Returns the result of applying {@code prefix} to this unit. * <p> 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. <br> + * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + * <p> + * If showUncertainty is true, strings like "35 ± 8" are shown instead of single + * numbers. + * <p> + * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(boolean showUncertainty) { + Optional<String> primaryName = this.unit.getPrimaryName(); + Optional<String> 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); + } + } + } +} diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java new file mode 100644 index 0000000..9e565d9 --- /dev/null +++ b/src/org/unitConverter/unit/UnitValue.java @@ -0,0 +1,114 @@ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +/** + * A value expressed in a unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class UnitValue { + /** + * @param unit unit to use + * @param value value to use + * @return {@code UnitValue} instance + */ + public static UnitValue of(Unit unit, double value) { + return new UnitValue(Objects.requireNonNull(unit, "unit must not be null"), value); + } + + private final Unit unit; + private final double value; + + /** + * @param unit the unit being used + * @param value the value being represented + */ + private UnitValue(Unit unit, double value) { + this.unit = unit; + this.value = value; + } + + /** + * @return the unit + */ + public final Unit getUnit() { + return unit; + } + + /** + * @return the value + */ + public final double getValue() { + return value; + } + + /** + * Converts this {@code UnitValue} into an equivalent {@code LinearUnitValue} by + * using this unit's base 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}. + */ + public final boolean canConvertTo(Unit other) { + return this.unit.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.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns true if this and obj represent the same value, regardless of whether + * or not they are expressed in the same unit. So (1000 m).equals(1 km) returns + * true. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnitValue)) + return false; + final UnitValue other = (UnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double + .doubleToLongBits(other.unit.convertToBase(other.getValue())); + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), this.unit.convertFromBase(this.getValue())); + } + + @Override + public String toString() { + Optional<String> primaryName = this.getUnit().getPrimaryName(); + Optional<String> 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()); + } else { + String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} |