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}