diff options
Diffstat (limited to 'src/main/java/org/unitConverter/math/UncertainDouble.java')
-rw-r--r-- | src/main/java/org/unitConverter/math/UncertainDouble.java | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/src/main/java/org/unitConverter/math/UncertainDouble.java b/src/main/java/org/unitConverter/math/UncertainDouble.java new file mode 100644 index 0000000..3651bd5 --- /dev/null +++ b/src/main/java/org/unitConverter/math/UncertainDouble.java @@ -0,0 +1,419 @@ +/** + * 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 <https://www.gnu.org/licenses/>. + */ +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. + * <p> + * 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<UncertainDouble> { + /** + * 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. + * <p> + * 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 <b>absolute</b> + * 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 <b>relative</b> + * 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}. + * <p> + * 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. + * <p> + * <b>Note:</b> 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}. + * <p> + * This method returns the same value as {@link #toString(boolean)}, but + * {@code showUncertainty} is true if and only if the uncertainty is + * non-zero. + * + * <p> + * Examples: + * + * <pre> + * 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" + * </pre> + * + * @since 2020-09-07 + */ + @Override + public final String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + * <p> + * 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" + * <p> + * 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.<br> + * 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. + * <p> + * Examples: + * + * <pre> + * 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" + * </pre> + * + * @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; + } +} |