diff options
Diffstat (limited to 'src/org/unitConverter/unit/LinearUnitValue.java')
-rw-r--r-- | src/org/unitConverter/unit/LinearUnitValue.java | 300 |
1 files changed, 300 insertions, 0 deletions
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); + } + } + } +} |