summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/org/unitConverter/unit/LinearUnit.java12
-rw-r--r--src/org/unitConverter/unit/LinearUnitValue.java300
-rw-r--r--src/org/unitConverter/unit/UnitValue.java114
3 files changed, 420 insertions, 6 deletions
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
index 1e5ae53..762572a 100644
--- a/src/org/unitConverter/unit/LinearUnit.java
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -246,16 +246,16 @@ public final class LinearUnit extends Unit {
* @since 2019-03-17
* @since v0.2.0
*/
- public LinearUnit minus(final LinearUnit subtrahendend) {
- Objects.requireNonNull(subtrahendend, "addend must not be null.");
+ public LinearUnit minus(final LinearUnit subtrahend) {
+ Objects.requireNonNull(subtrahend, "addend must not be null.");
// reject subtrahends that cannot be added to this unit
- if (!this.getBase().equals(subtrahendend.getBase()))
+ if (!this.getBase().equals(subtrahend.getBase()))
throw new IllegalArgumentException(
- String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend));
+ String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahend));
// subtract the units
- return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor());
+ return valueOf(this.getBase(), this.getConversionFactor() - subtrahend.getConversionFactor());
}
/**
@@ -347,7 +347,7 @@ public final class LinearUnit extends Unit {
public LinearUnit withName(final NameSymbol ns) {
return valueOf(this.getBase(), this.getConversionFactor(), ns);
}
-
+
/**
* Returns the result of applying {@code prefix} to this unit.
* <p>
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);
+ }
+ }
+ }
+}
diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java
new file mode 100644
index 0000000..9e565d9
--- /dev/null
+++ b/src/org/unitConverter/unit/UnitValue.java
@@ -0,0 +1,114 @@
+package org.unitConverter.unit;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * A value expressed in a 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 UnitValue {
+ /**
+ * @param unit unit to use
+ * @param value value to use
+ * @return {@code UnitValue} instance
+ */
+ public static UnitValue of(Unit unit, double value) {
+ return new UnitValue(Objects.requireNonNull(unit, "unit must not be null"), value);
+ }
+
+ private final Unit unit;
+ private final double value;
+
+ /**
+ * @param unit the unit being used
+ * @param value the value being represented
+ */
+ private UnitValue(Unit unit, double value) {
+ this.unit = unit;
+ this.value = value;
+ }
+
+ /**
+ * @return the unit
+ */
+ public final Unit getUnit() {
+ return unit;
+ }
+
+ /**
+ * @return the value
+ */
+ public final double getValue() {
+ return value;
+ }
+
+ /**
+ * Converts this {@code UnitValue} into an equivalent {@code LinearUnitValue} by
+ * using this unit's base unit.
+ *
+ * @param newName A new name for the base unit. Use {@link NameSymbol#EMPTY} if
+ * you don't want one.
+ */
+ public final LinearUnitValue asLinearUnitValue(NameSymbol newName) {
+ LinearUnit base = LinearUnit.valueOf(unit.getBase(), 1, newName);
+ return LinearUnitValue.getExact(base, base.convertToBase(value));
+ }
+
+ /**
+ * @param other a {@code Unit}
+ * @return true iff this value can be represented with {@code other}.
+ */
+ public final boolean canConvertTo(Unit other) {
+ return this.unit.canConvertTo(other);
+ }
+
+ /**
+ * Returns a UnitValue that represents the same value expressed in a
+ * different unit
+ *
+ * @param other new unit to express value in
+ * @return value expressed in {@code other}
+ */
+ public final UnitValue convertTo(Unit other) {
+ return UnitValue.of(other, this.getUnit().convertTo(other, this.getValue()));
+ }
+
+ /**
+ * 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.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof UnitValue))
+ return false;
+ final UnitValue other = (UnitValue) obj;
+ return Objects.equals(this.unit.getBase(), other.unit.getBase())
+ && Double.doubleToLongBits(this.unit.convertToBase(this.getValue())) == Double
+ .doubleToLongBits(other.unit.convertToBase(other.getValue()));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.unit.getBase(), this.unit.convertFromBase(this.getValue()));
+ }
+
+ @Override
+ public String toString() {
+ Optional<String> primaryName = this.getUnit().getPrimaryName();
+ Optional<String> symbol = this.getUnit().getSymbol();
+ if (primaryName.isEmpty() && symbol.isEmpty()) {
+ double baseValue = this.getUnit().convertToBase(this.getValue());
+ return String.format("%s unnamed unit (= %s %s)", this.getValue(), baseValue, this.getUnit().getBase());
+ } else {
+ String unitName = symbol.orElse(primaryName.get());
+ return this.getValue() + " " + unitName;
+ }
+ }
+}