/**
* 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
* 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
* 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 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)}, but
* {@code showUncertainty} is true if and only if the uncertainty is
* non-zero.
*
*
* Examples:
*
*
* 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.
* 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());
}
/**
* Returns a string representation of this {@code UncertainDouble}.
*
* 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.
*
* 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) {
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;
}
}