001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.units;
006
007import java.lang.reflect.Constructor;
008import java.lang.reflect.InvocationTargetException;
009import java.util.Objects;
010
011/**
012 * Builder used for easily deriving new units from existing ones.
013 *
014 * @param <U> the type of the unit
015 */
016public final class UnitBuilder<U extends Unit<U>> {
017  private final U m_base;
018  private UnaryFunction m_fromBase;
019  private UnaryFunction m_toBase;
020  private String m_name;
021  private String m_symbol;
022
023  /**
024   * Creates a new unit builder object, building off of a base unit. The base unit does not have to
025   * be <i>the</i> base unit of its unit system; furlongs work just as well here as meters.
026   *
027   * @param base the unit to base the new unit off of
028   */
029  public UnitBuilder(U base) {
030    this.m_base = Objects.requireNonNull(base, "Base unit cannot be null");
031  }
032
033  /**
034   * Sets the unit conversions based on a simple offset. The new unit will have its values equal to
035   * (base value - offset).
036   *
037   * @param offset the offset
038   * @return this builder
039   */
040  public UnitBuilder<U> offset(double offset) {
041    m_toBase = derivedValue -> derivedValue + offset;
042    m_fromBase = baseValue -> baseValue - offset;
043    return this;
044  }
045
046  /**
047   * Maps a value {@code value} in the range {@code [inMin..inMax]} to an output in the range {@code
048   * [outMin..outMax]}. Inputs outside the bounds will be mapped correspondingly to outputs outside
049   * the output bounds. Inputs equal to {@code inMin} will be mapped to {@code outMin}, and inputs
050   * equal to {@code inMax} will similarly be mapped to {@code outMax}.
051   *
052   * @param value the value to map
053   * @param inMin the minimum input value (does not have to be absolute)
054   * @param inMax the maximum input value (does not have to be absolute)
055   * @param outMin the minimum output value (does not have to be absolute)
056   * @param outMax the maximum output value (does not have to be absolute)
057   * @return the mapped output
058   */
059  // NOTE: This method lives here instead of in MappingBuilder because inner classes can't
060  // define static methods prior to Java 16.
061  private static double mapValue(
062      double value, double inMin, double inMax, double outMin, double outMax) {
063    return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
064  }
065
066  /** Helper class used for safely chaining mapping builder calls. */
067  public class MappingBuilder {
068    private final double m_minInput;
069    private final double m_maxInput;
070
071    private MappingBuilder(double minInput, double maxInput) {
072      this.m_minInput = minInput;
073      this.m_maxInput = maxInput;
074    }
075
076    /**
077     * Finalizes the mapping by defining the output range.
078     *
079     * @param minOutput the minimum output value (does not have to be absolute)
080     * @param maxOutput the maximum output value (does not have to be absolute)
081     * @return the unit builder, for continued chaining
082     */
083    public UnitBuilder<U> toOutputRange(double minOutput, double maxOutput) {
084      UnitBuilder.this.m_fromBase = x -> mapValue(x, m_minInput, m_maxInput, minOutput, maxOutput);
085      UnitBuilder.this.m_toBase = y -> mapValue(y, minOutput, maxOutput, m_minInput, m_maxInput);
086      return UnitBuilder.this;
087    }
088  }
089
090  /**
091   * Defines a mapping for values within the given input range. This method call should be
092   * immediately followed by {@code .toOutputRange}, eg {@code mappingInputRange(1,
093   * 2).toOutputRange(3, 4)}, which will return the unit builder for continued chaining.
094   *
095   * @param minBase the minimum input value (does not have to be absolute)
096   * @param maxBase the maximum output value (does not have to be absolute)
097   * @return a builder object used to define the output range
098   */
099  public MappingBuilder mappingInputRange(double minBase, double maxBase) {
100    return new MappingBuilder(minBase, maxBase);
101  }
102
103  /**
104   * Sets the conversion function to transform values in the base unit to values in the derived
105   * unit.
106   *
107   * @param fromBase the conversion function
108   * @return the unit builder, for continued chaining
109   */
110  public UnitBuilder<U> fromBase(UnaryFunction fromBase) {
111    this.m_fromBase = Objects.requireNonNull(fromBase, "fromBase function cannot be null");
112    return this;
113  }
114
115  /**
116   * Sets the conversion function to transform values in the derived unit to values in the base
117   * unit.
118   *
119   * @param toBase the conversion function
120   * @return the unit builder, for continued chaining
121   */
122  public UnitBuilder<U> toBase(UnaryFunction toBase) {
123    this.m_toBase = Objects.requireNonNull(toBase, "toBase function cannot be null");
124    return this;
125  }
126
127  /**
128   * Sets the name of the new unit.
129   *
130   * @param name the new name
131   * @return the unit builder, for continued chaining
132   */
133  public UnitBuilder<U> named(String name) {
134    this.m_name = name;
135    return this;
136  }
137
138  /**
139   * Sets the symbol of the new unit.
140   *
141   * @param symbol the new symbol
142   * @return the unit builder, for continued chaining
143   */
144  public UnitBuilder<U> symbol(String symbol) {
145    this.m_symbol = symbol;
146    return this;
147  }
148
149  /**
150   * Helper for defining units that are a scalar fraction of the base unit, such as centimeters
151   * being 1/100th of the base unit (meters). The fraction value is specified as the denominator of
152   * the fraction, so a centimeter definition would use {@code splitInto(100)} instead of {@code
153   * splitInto(1/100.0)}.
154   *
155   * @param fraction the denominator portion of the fraction of the base unit that a value of 1 in
156   *     the derived unit corresponds to
157   * @return the unit builder, for continued chaining
158   */
159  public UnitBuilder<U> splitInto(double fraction) {
160    if (fraction == 0) {
161      throw new IllegalArgumentException("Fraction must be nonzero");
162    }
163
164    return toBase(x -> x / fraction).fromBase(b -> b * fraction);
165  }
166
167  /**
168   * Helper for defining units that are a scalar multiple of the base unit, such as kilometers being
169   * 1000x of the base unit (meters).
170   *
171   * @param aggregation the magnitude required for a measure in the base unit to equal a magnitude
172   *     of 1 in the derived unit
173   * @return the unit builder, for continued chaining
174   */
175  public UnitBuilder<U> aggregate(double aggregation) {
176    if (aggregation == 0) {
177      throw new IllegalArgumentException("Aggregation amount must be nonzero");
178    }
179
180    return toBase(x -> x * aggregation).fromBase(b -> b / aggregation);
181  }
182
183  /**
184   * Creates the new unit based off of the builder methods called prior.
185   *
186   * @return the new derived unit
187   * @throws NullPointerException if the unit conversions, unit name, or unit symbol were not set
188   * @throws RuntimeException if the base unit does not define a constructor accepting the
189   *     conversion functions, unit name, and unit symbol - in that order
190   */
191  @SuppressWarnings("PMD.AvoidAccessibilityAlteration")
192  public U make() {
193    Objects.requireNonNull(m_fromBase, "fromBase function was not set");
194    Objects.requireNonNull(m_toBase, "toBase function was not set");
195    Objects.requireNonNull(m_name, "new unit name was not set");
196    Objects.requireNonNull(m_symbol, "new unit symbol was not set");
197    Class<? extends U> baseType = m_base.m_baseType;
198    try {
199      Constructor<? extends U> ctor =
200          baseType.getDeclaredConstructor(
201              UnaryFunction.class, // toBaseUnits
202              UnaryFunction.class, // fromBaseUnits
203              String.class, // name
204              String.class); // symbol
205      // need to flag the constructor as accessible so we can use private, package-private, and
206      // protected constructors
207      ctor.setAccessible(true);
208      return ctor.newInstance(
209          m_toBase.pipeTo(m_base.getConverterToBase()),
210          m_base.getConverterFromBase().pipeTo(m_fromBase),
211          m_name,
212          m_symbol);
213    } catch (InstantiationException e) {
214      throw new RuntimeException("Could not instantiate class " + baseType.getName(), e);
215    } catch (IllegalAccessException e) {
216      throw new RuntimeException("Could not access constructor", e);
217    } catch (InvocationTargetException e) {
218      throw new RuntimeException("Constructing " + baseType.getName() + " raised an exception", e);
219    } catch (NoSuchMethodException e) {
220      throw new RuntimeException("No compatible constructor", e);
221    }
222  }
223}