/**
* 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 org.unitConverter.unit;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.DoubleUnaryOperator;
import org.unitConverter.math.ObjectProduct;
/**
* A unit that is composed of base units.
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
public abstract class Unit {
/**
* 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;
/**
* The primary name used by this unit.
*/
private final Optional primaryName;
/**
* A short symbol used to represent this unit.
*/
private final Optional symbol;
/**
* A set of any additional names and/or spellings that the unit uses.
*/
private final Set otherNames;
/**
* Cache storing the result of getDimension()
*
* @since 2019-10-16
*/
private transient ObjectProduct dimension = null;
/**
* Creates the {@code AbstractUnit}.
*
* @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(final ObjectProduct unitBase, final NameSymbol ns) {
this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null.");
this.primaryName = Objects.requireNonNull(ns, "ns must not be null.").getPrimaryName();
this.symbol = ns.getSymbol();
this.otherNames = ns.getOtherNames();
}
/**
* A constructor that constructs {@code BaseUnit} instances.
*
* @since 2019-10-16
*/
Unit(final String primaryName, final String symbol, final Set otherNames) {
if (this instanceof BaseUnit) {
this.unitBase = ObjectProduct.oneOf((BaseUnit) this);
} else
throw new AssertionError();
this.primaryName = Optional.of(primaryName);
this.symbol = Optional.of(symbol);
this.otherNames = Collections.unmodifiableSet(
new HashSet<>(Objects.requireNonNull(otherNames, "additionalNames must not be null.")));
}
/**
* Checks if a value expressed in this unit can be converted to a value expressed in {@code other}
*
* @param other
* unit to test with
* @return true if the units 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 additionalNames
* @since 2019-10-21
*/
public final Set getOtherNames() {
return this.otherNames;
}
/**
* @return primaryName
* @since 2019-10-21
*/
public final Optional getPrimaryName() {
return this.primaryName;
}
/**
* @return symbol
* @since 2019-10-21
*/
public final Optional getSymbol() {
return this.symbol;
}
@Override
public String toString() {
return this.getPrimaryName().orElse("Unnamed unit")
+ (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "")
+ ", derived from " + this.getBase().toString(u -> u.getSymbol().get())
+ (this.getOtherNames().isEmpty() ? "" : ", also called " + String.join(", ", this.getOtherNames()));
}
/**
* @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."));
}
}