/** * */ 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); } } } }