/** * Copyright (C) 2019 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.unit; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** * A unit that is composed of base units. * * @author Adrien Hopkins * @since 2019-10-16 */ public abstract class Unit implements Nameable { /** * Returns a unit from its base and the functions it uses to convert to and * from its base. * *

* For example, to get a unit representing the degree Celsius, the following * code can be used: * * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} *

* * @param base unit's base * @param converterFrom function that accepts a value expressed in the unit's * base and returns that value expressed in this unit. * @param converterTo function that accepts a value expressed in the unit * and returns that value expressed in the unit's base. * @return a unit that uses the provided functions to convert. * @since 2019-05-22 * @throws NullPointerException if any argument is null */ public static final Unit fromConversionFunctions( final ObjectProduct base, final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { return new FunctionalUnit(base, converterFrom, converterTo); } /** * Returns a unit from its base and the functions it uses to convert to and * from its base. * *

* For example, to get a unit representing the degree Celsius, the following * code can be used: * * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} *

* * @param base unit's base * @param converterFrom function that accepts a value expressed in the unit's * base and returns that value expressed in this unit. * @param converterTo function that accepts a value expressed in the unit * and returns that value expressed in the unit's base. * @param ns names and symbol of unit * @return a unit that uses the provided functions to convert. * @since 2019-05-22 * @throws NullPointerException if any argument is null */ public static final Unit fromConversionFunctions( final ObjectProduct base, final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo, final NameSymbol ns) { return new FunctionalUnit(base, converterFrom, converterTo, ns); } /** * The combination of units that this unit is based on. * * @since 2019-10-16 */ private final ObjectProduct unitBase; /** * This unit's name(s) and symbol * * @since 2020-09-07 */ private final NameSymbol nameSymbol; /** * Cache storing the result of getDimension() * * @since 2019-10-16 */ private transient ObjectProduct dimension = null; /** * A constructor that constructs {@code BaseUnit} instances. * * @since 2019-10-16 */ Unit(final NameSymbol nameSymbol) { if (this instanceof BaseUnit) { this.unitBase = ObjectProduct.oneOf((BaseUnit) this); } else throw new AssertionError(); this.nameSymbol = nameSymbol; } /** * Creates the {@code Unit}. * * @param unitBase base of unit * @param ns names and symbol of unit * @since 2019-10-16 * @throws NullPointerException if unitBase or ns is null */ protected Unit(ObjectProduct unitBase, NameSymbol ns) { this.unitBase = Objects.requireNonNull(unitBase, "unitBase may not be null"); this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); } /** * Checks if a value expressed in this unit can be converted to a value * expressed in {@code other} * * @param other unit or unitlike form to test with * @return true if they are compatible * @since 2019-01-13 * @since v0.1.0 * @throws NullPointerException if other is null */ public final boolean canConvertTo(final Unit other) { Objects.requireNonNull(other, "other must not be null."); return Objects.equals(this.getBase(), other.getBase()); } /** * Converts from a value expressed in this unit's base unit to a value * expressed in this unit. *

* This must be the inverse of {@code convertToBase}, so * {@code convertFromBase(convertToBase(value))} must be equal to * {@code value} for any value, ignoring precision loss by roundoff error. *

*

* If this unit is a base unit, this method should return * {@code value}. *

* * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. * * @param value value expressed in base unit * @return value expressed in this unit * @since 2018-12-22 * @since v0.1.0 */ protected abstract double convertFromBase(double value); /** * Converts a value expressed in this unit to a value expressed in * {@code other}. * * @implSpec If unit conversion is possible, this implementation returns * {@code other.convertFromBase(this.convertToBase(value))}. * Therefore, overriding either of those methods will change the * output of this method. * * @param other unit to convert to * @param value value to convert * @return converted value * @since 2019-05-22 * @throws IllegalArgumentException if {@code other} is incompatible for * conversion with this unit (as tested by * {@link Unit#canConvertTo}). * @throws NullPointerException if other is null */ public final double convertTo(final Unit other, final double value) { Objects.requireNonNull(other, "other must not be null."); if (this.canConvertTo(other)) return other.convertFromBase(this.convertToBase(value)); else throw new IllegalArgumentException( String.format("Cannot convert from %s to %s.", this, other)); } /** * Converts from a value expressed in this unit to a value expressed in this * unit's base unit. *

* This must be the inverse of {@code convertFromBase}, so * {@code convertToBase(convertFromBase(value))} must be equal to * {@code value} for any value, ignoring precision loss by roundoff error. *

*

* If this unit is a base unit, this method should return * {@code value}. *

* * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. * * @param value value expressed in this unit * @return value expressed in base unit * @since 2018-12-22 * @since v0.1.0 */ protected abstract double convertToBase(double value); /** * @return combination of units that this unit is based on * @since 2018-12-22 * @since v0.1.0 */ public final ObjectProduct getBase() { return this.unitBase; } /** * @return dimension measured by this unit * @since 2018-12-22 * @since v0.1.0 */ public final ObjectProduct getDimension() { if (this.dimension == null) { final Map mapping = this.unitBase.exponentMap(); final Map dimensionMap = new HashMap<>(); for (final BaseUnit key : mapping.keySet()) { dimensionMap.put(key.getBaseDimension(), mapping.get(key)); } this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); } return this.dimension; } /** * @return the nameSymbol * @since 2020-09-07 */ @Override public final NameSymbol getNameSymbol() { return this.nameSymbol; } /** * Returns true iff this unit is metric. *

* "Metric" is defined by three conditions: *

    *
  • Must be an instance of {@link LinearUnit}.
  • *
  • Must be based on the SI base units (as determined by getBase())
  • *
  • The conversion factor must be a power of 10.
  • *
*

* Note that this definition excludes some units that many would consider * "metric", such as the degree Celsius (fails the first condition), * calories, minutes and hours (fail the third condition). *

* All SI units (as designated by the BIPM) except the degree Celsius are * considered "metric" by this definition. * * @since 2020-08-27 */ public final boolean isMetric() { // first condition - check that it is a linear unit if (!(this instanceof LinearUnit)) return false; final LinearUnit linear = (LinearUnit) this; // second condition - check that for (final BaseUnit b : linear.getBase().getBaseSet()) { if (!Metric.BaseUnits.BASE_UNITS.contains(b)) return false; } // third condition - check that conversion factor is a power of 10 return DecimalComparison .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); } /** * @return a string representing this unit's definition * @since 2022-03-10 */ public String toDefinitionString() { if (!this.unitBase.getNameSymbol().isEmpty()) return "derived from " + this.unitBase.getName(); else return "derived from " + this.getBase().toString(BaseUnit::getShortName); } /** * @return a string containing both this unit's name and its definition * @since 2022-03-10 */ public final String toFullString() { return this.toString() + " (" + this.toDefinitionString() + ")"; } @Override public String toString() { if (this.nameSymbol.getPrimaryName().isPresent() && this.nameSymbol.getSymbol().isPresent()) return this.nameSymbol.getPrimaryName().orElseThrow() + " (" + this.nameSymbol.getSymbol().orElseThrow() + ")"; else return this.getName(); } /** * @param ns name(s) and symbol to use * @return a copy of this unit with provided name(s) and symbol * @since 2019-10-21 * @throws NullPointerException if ns is null */ public Unit withName(final NameSymbol ns) { return fromConversionFunctions(this.getBase(), this::convertFromBase, this::convertToBase, Objects.requireNonNull(ns, "ns must not be null.")); } }