/** * 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 sevenUnits.utils; 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); static final String NUMBER_REGEX = "(\\d+(?:[\\.,]\\d+))"; /** * A regular expression that can recognize toString forms */ static final Pattern TO_STRING = Pattern.compile(NUMBER_REGEX // optional "± [number]" + "(?:\\s*(?:±|\\+-)\\s*" + NUMBER_REGEX + ")?"); /** * Gets an UncertainDouble from a double string. The uncertainty of the * double will be one of the lowest decimal place of the number. For example, * "12345.678" will become 12345.678 ± 0.001. * * @throws NumberFormatException if the argument is not a number * * @since 2022-04-18 */ public static final UncertainDouble fromRoundedString(String s) { final BigDecimal value = new BigDecimal(s); final double uncertainty = Math.pow(10, -value.scale()); return UncertainDouble.of(value.doubleValue(), uncertainty); } /** * Parses a string in the form of {@link UncertainDouble#toString(boolean, RoundingMode)} * 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); if (!matcher.matches()) throw new IllegalArgumentException( "Could not parse string \"" + s + "\"."); double value, uncertainty; try { value = Double.parseDouble(matcher.group(1)); } catch (final 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.compare(this.value, other.value) != 0) return false; if (Double.compare(this.uncertainty, other.uncertainty) != 0) 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; result = prime * result + Double.hashCode(this.value); result = prime * result + Double.hashCode(this.uncertainty); 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, RoundingMode)}, * 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(), RoundingMode.HALF_EVEN); } /** * 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, RoundingMode roundingMode) { 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); final BigDecimal roundedValue = bigValue.setScale(displayScale, roundingMode); 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; } }