UnitConverter.java

package de.turnertech.measures;

import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Objects;
import java.util.function.DoubleUnaryOperator;

/**
 * The UnitConverter handles specialist conversions with more accuracy than would be possible
 * via conversions to base units. For example if converting from a unit like Kilometer to Nautical 
 * Mile, it is possble that converting first to meters would cause overrun errors or lose accuracy
 * due to binary limitations.
 * 
 * 1. Functions
 * 2. Scalars
 * 3. Divisors (Scalar)
 * 4. Unit Base Unit
 */
public class UnitConverter {
    
    private static final HashMap<AbstractMap.SimpleImmutableEntry<Unit, Unit>, Double> scalarMap = new HashMap<>();
    private static final HashMap<AbstractMap.SimpleImmutableEntry<Unit, Unit>, DoubleUnaryOperator> functionMap = new HashMap<>();

    static {
        scalarMap.put(new AbstractMap.SimpleImmutableEntry<>(Unit.NAUTICAL_MILE, Unit.CENTIMETRE), 185200.0);
        scalarMap.put(new AbstractMap.SimpleImmutableEntry<>(Unit.NAUTICAL_MILE, Unit.METRE), 1852.0);
        scalarMap.put(new AbstractMap.SimpleImmutableEntry<>(Unit.NAUTICAL_MILE, Unit.KILOMETRE), 1.852);
        scalarMap.put(new AbstractMap.SimpleImmutableEntry<>(Unit.KILOMETRE, Unit.METRE), 1000.0);
        scalarMap.put(new AbstractMap.SimpleImmutableEntry<>(Unit.KILOMETRE, Unit.CENTIMETRE), 100000.0);
        
        // Placeholder. This is a bad example of using functions to optimise. REplace in the future with something like KNOT -> METRE_PER_SECOND (knots) - > (knots / 3.6) * 1.852
        functionMap.put(new AbstractMap.SimpleImmutableEntry<>(Unit.NAUTICAL_MILE, Unit.KILOMETRE), (nmi_i) -> nmi_i * 1.852);
    }

    /**
     * <p>Adds a scalar function for transitioning from the unitIn, to the 
     * unitOut. This scalar will also be used as a divisor in the reverse 
     * direction.</p>
     * 
     * @param unitIn the Unit in which the in parameter is represented.
     * @param unitOut the desired Unit which should be present in the returned.
     * @param scalar the scalar.
     * @return the result of Map.put(...)
     */
    public static Double putScalar(final Unit unitIn, final Unit unitOut, final double scalar) {
        if(scalar == 0.0) {
            throw new ArithmeticException("scalar values of 0 are not accepted in the UnitConverter.");
        }
        return scalarMap.put(new AbstractMap.SimpleImmutableEntry<>(Objects.requireNonNull(unitIn), Objects.requireNonNull(unitOut)), scalar);
    }

    /**
     * <p>Adds a conversion function for transitioning from the unitIn, to the
     * unitOut. This should be the highest accuracy possible and will be 
     * prioritised over other conversion methods.</p>
     * 
     * @param unitIn the Unit in which the in parameter is represented.
     * @param unitOut the desired Unit which should be present in the returned.
     * @param function the conversion function.
     * @return the response to Map.put(...)
     */
    public static DoubleUnaryOperator putFunction(final Unit unitIn, final Unit unitOut, final DoubleUnaryOperator function) {
        return functionMap.put(new AbstractMap.SimpleImmutableEntry<>(Objects.requireNonNull(unitIn), Objects.requireNonNull(unitOut)), Objects.requireNonNull(function));
    }

    /**
     * <p>Converts between Units using one of a number of conversion options. 
     * The conversion method used is prioritised to enable accuracy.</p>
     * 
     * @param in Measure to convert.
     * @param unitOut Desired output Unit
     * @return a measure containing the in parameter represented in the unitOut.
     */
    public static Measure convert(final Measure in, final Unit unitOut) {
        Objects.requireNonNull(in);

        return convert(in.getQuantity(), in.getUnit(), unitOut);
    }

    /**
     * <p>Converts between Units using one of a number of conversion options. 
     * The conversion method used is prioritised to enable accuracy.</p>
     * 
     * <ol>
     * <li>Conversion Function stored in this class</li>
     * <li>Conversion Scalar stored in this class</li>
     * <li>Conversion Divisor using the Scalar stored in this class</li>
     * <li>Conversion to/from the base Units stored in the Unit class</li>
     * </ol>
     * 
     * @param in value to convert between Units.
     * @param unitIn the Unit in which the in parameter is represented.
     * @param unitOut the desired Unit which should be present in the returned
     * measure
     * @return a measure containing the in parameter represented in the unitOut.
     * @throws UnsupportedOperationException if conversion is not supported
     */
    public static Measure convert(final double in, final Unit unitIn, final Unit unitOut) {
        Objects.requireNonNull(unitIn);
        Objects.requireNonNull(unitOut);

        // Same Unit case
        if(unitIn == unitOut) {
            return new Measure(in, unitOut);
        }

        // Function case
        final DoubleUnaryOperator conversionFunction = functionMap.getOrDefault(new AbstractMap.SimpleImmutableEntry<>(unitIn, unitOut), null);
        if(conversionFunction != null) {
            return new Measure(conversionFunction.applyAsDouble(in), unitOut);
        }

        // Scalar case
        Double variable = scalarMap.getOrDefault(new AbstractMap.SimpleImmutableEntry<>(unitIn, unitOut), null);
        if(variable != null) {
            return new Measure(in * variable, unitOut);
        }

        // Divisor case
        variable = scalarMap.getOrDefault(new AbstractMap.SimpleImmutableEntry<>(unitOut, unitIn), null);
        if(variable != null) {
            return new Measure(in / variable, unitOut);
        }

        // Convert to shared base unit
        if(unitIn.getBaseUnit() == unitOut.getBaseUnit()) {
            final Measure inInBaseUnit = unitIn.convertToBaseUnit(in);
            return unitOut.convertFromBaseUnit(inInBaseUnit.getQuantity());
        }

        throw new UnsupportedOperationException("Conversion from " + unitIn.toString() + " to " + unitOut.toString() + " is not supported.");
    }

}