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