[wpiunits] Add subproject for a Java typesafe unit system (#5371)

# Background

Unit safety has always been a problem in WPILib. Any value corresponding to a physical measurement, such as current draw or distance traveled, is represented by a bare number with no unit tied to it; it's up to the programmer to know what units they're working and take care to remember that while working on their robot program. This leads to bugs when programmers accidentally mix units without knowing, or measure something (such as a wheel diameter) in one unit and program using another. `wpiunits` is intended to eliminate that class of bugs.

Another source of friction is the controllers and models in `wpimath` that expect all inputs to be in terms of SI units (meter, kilogram, and so on), while most FRC teams are US-based and most commonly use imperial units. wpimath does a good job of noting unit types in method names and argument names; however, it still relies on users properly converting values (and knowing they even have to do so).

# API

There are really only two core classes in this library: `Unit` and `Measure`. A `Unit` represents some dimension like distance or time. `Unit` is subclassed to define specific dimensions (eg `Distance` and `Time`) and those subclasses are instantiated to defined particular units in those dimensions, such as `Meters` and `Feet` being instances of the `Distance` class.

A `Measure` is a value tied to a particular dimension like distance and knows what unit that value is tied to. `Measure` has two implementations - one immutable and one mutable. The `Measure` interface only defines *read-only* operations; any API working with measurements should use the interface. The default implementation is `ImmutableMeasure`, which only implements those read-only operations and is useful for tracking constants. `MutableMeasure` also adds some methods that will allow for mutation of its internal state; this class is intended for use for things like sensors and controllers that track internal state and don't want to allocate new `Measure` objects every time something like `myEncoder.getDistance()` is called. However, the APIs for those methods should still only expose the read-only `Measure` interface so users can't (without casting or reflection) change the internal values.

A `Units` class provides convenient definitions for most of the commonly used unit types, such as `Meters`, `Feet`, and `Milliseconds`. I recommend static importing these units eg `import static edu.wpi.first.units.Units.Meters`) so they can be used like `Meters.of(1.234)` instead of `Units.Meters.of(1.234)`


# Examples

These examples are admittedly contrived. Users shouldn't be interacting much with measure objects themselves, since wpimath and wpilibj classes will be updated to support working with them; users will often just have to take a `Measure` output from one place (such as an encoder) and feed it as input to something else (such as a PID controller or kinematics model)

```java
// Using raw units
Encoder encoder = ...

int kPulsesPerRev = 2048;
double kWheelDiameterMeters = Units.inchesToMeters(6);
double kGearRatio = 10.86;
 // always have to remember this encoder will output in meters!
encoder.setDistancePerPulse(kWheelDiameterMeters * Math.PI / (kGearRatio * kPulsesPerRev));

Command driveDistance(double distance) {
  // have to know the distance argument needs to be in meters!
  return run(this::driveStraight).until(() -> encoder.getDistance() >= distance);
}

// Oops! This will go 16 feet, not 5!
Command driveFiveFeet = driveDistance(5);
Command driveOneMeter = driveDistance(1);
```
```java
// Using wpiunits

Encoder encoder = ...

int kPulsesPerRev = 2048;
Measure<Distance> kWheelDiameter = Inches.of(6);
double kGearRatio = 10.86;
encoder.setDistancePerPulse(kWheelDiameter.times(Math.PI).divide(kGearRatio * kPulsesPerRev));

Command driveDistance(Measure<Distance> distance) {
  // Measure#gte automatically handles unit conversions
  return run(this::driveStraight).until(() -> encoder.getDistance().gte(distance));
}

// Users HAVE to be explicit about their units
Command driveFiveFeet = driveDistance(Feet.of(5));
Command driveOneMeter = driveDistance(Meters.of(1));
```

```java
SmartDashboard.putNumber("Temperature (C)", pdp.getTemperature().in(Celsius));
SmartDashboard.putNumber("Temperature (F)", pdp.getTemperature().in(Fahrenheit));
```

```java
var InchSecond = Inch.mult(Second); // new combined unit types can be user-defined
var InchPerSecond = Inch.per(Second);

PIDController<Distance, ElectricPotential> heightController = new PIDController<>(
  /* kP */ Volts.of(0.2).per(Inch),
  /* kI */ Volts.of(0.002).per(InchSecond),
  /* kD */ Volts.of(0.008).per(InchPerSecond)
);

var elevatorTop = Feet.of(4).plus(Inches.of(6.125));
elevatorMotor.setVoltage(heightController.calculate(encoder.getDistance(), elevatorTop));
```
This commit is contained in:
Sam Carlberg
2023-07-23 17:18:17 -04:00
committed by GitHub
parent 44acca7c00
commit c065ae1fcf
40 changed files with 3931 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ evaluationDependsOn(':wpilibc')
evaluationDependsOn(':wpilibj')
evaluationDependsOn(':wpimath')
evaluationDependsOn(':wpinet')
evaluationDependsOn(':wpiunits')
evaluationDependsOn(':wpiutil')
def baseArtifactIdCpp = 'documentation'

View File

@@ -33,6 +33,7 @@ include 'wpilibcIntegrationTests'
include 'wpilibjExamples'
include 'wpilibjIntegrationTests'
include 'wpilibj'
include 'wpiunits'
include 'crossConnIntegrationTests'
include 'fieldImages'
include 'glass'

14
wpiunits/build.gradle Normal file
View File

@@ -0,0 +1,14 @@
ext {
useJava = true
useCpp = false
baseId = 'wpiunits'
groupId = 'edu.wpi.first.wpiunits'
nativeName = 'wpiunits'
devMain = 'edu.wpi.first.units.DevMain'
}
apply from: "${rootDir}/shared/java/javacommon.gradle"
dependencies {
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public final class DevMain {
/** Main entry point. */
public static void main(String[] args) {
System.out.println(Units.Inches.of(-5.0).in(Units.Meters));
}
private DevMain() {}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
// technically, angles are unitless dimensions
// eg Mass * Distance * Velocity<Angle> is equivalent to (Mass * Distance) / Time - otherwise known
// as Power - in other words, Velocity<Angle> is /actually/ Frequency
public class Angle extends Unit<Angle> {
/**
* Creates a new unit with the given name and multiplier to the base unit.
*
* @param baseUnitEquivalent the multiplier to convert this unit to the base unit of this type
* @param name the name of the angle measure
* @param symbol the symbol of the angle measure
*/
Angle(double baseUnitEquivalent, String name, String symbol) {
super(Angle.class, baseUnitEquivalent, name, symbol);
}
Angle(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Angle.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
/** The base units of measure. */
public final class BaseUnits {
private BaseUnits() {
// Prevent instantiation
}
/** The standard unit of distance, meters. */
public static final Distance Distance = new Distance(1, "Meter", "m");
/** The standard unit of time, seconds. */
public static final Time Time = new Time(1, "Second", "s");
/** The standard unit of velocity, meters per second. */
public static final Velocity<Distance> Velocity =
new Velocity<>(Distance, Time, "Meter per Second", "m/s");
/** The standard unit of mass, grams. */
public static final Mass Mass = new Mass(1, "Kilogram", "Kg");
/** The standard unit of angles, revolutions. */
public static final Angle Angle = new Angle(1, "Revolution", "R");
/** The standard "unitless" unit. */
public static final Dimensionless Value = new Dimensionless(1, "<?>", "<?>");
/** The standard unit of voltage, volts. */
public static final Voltage Voltage = new Voltage(1, "Volt", "V");
/** The standard unit of electric current, amperes. */
public static final Current Current = new Current(1, "Amp", "A");
/** The standard unit of energy, joules. */
public static final Energy Energy = new Energy(1, "Joule", "J");
/** The standard unit of power, watts. */
public static final Power Power = new Power(1, "Watt", "W");
public static final Temperature Temperature = new Temperature(x -> x, x -> x, "Kelvin", "K");
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Current extends Unit<Current> {
Current(double baseUnitEquivalent, String name, String symbol) {
super(Current.class, baseUnitEquivalent, name, symbol);
}
Current(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Current.class, toBaseConverter, fromBaseConverter, name, symbol);
}
public Power times(Unit<Voltage> voltage, String name, String symbol) {
return new Power(this.toBaseUnits(1) * voltage.toBaseUnits(1), name, symbol);
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
/**
* A type of unit that corresponds to raw values and not any physical dimension, such as percentage.
*/
public class Dimensionless extends Unit<Dimensionless> {
/**
* Creates a new unit with the given name and multiplier to the base unit.
*
* @param baseUnitEquivalent the multiplier to convert this unit to the base unit of this type.
* @param name the name of the unit
* @param symbol the symbol of the unit
*/
protected Dimensionless(double baseUnitEquivalent, String name, String symbol) {
super(Dimensionless.class, baseUnitEquivalent, name, symbol);
}
Dimensionless(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Dimensionless.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Distance extends Unit<Distance> {
/** Creates a new unit with the given name and multiplier to the base unit. */
Distance(double baseUnitEquivalent, String name, String symbol) {
super(Distance.class, baseUnitEquivalent, name, symbol);
}
Distance(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Distance.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Energy extends Unit<Energy> {
protected Energy(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Energy.class, toBaseConverter, fromBaseConverter, name, symbol);
}
protected Energy(double baseUnitEquivalent, String name, String symbol) {
super(Energy.class, baseUnitEquivalent, name, symbol);
}
}

View File

@@ -0,0 +1,110 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import java.util.Objects;
/**
* A measure holds the magnitude and unit of some dimension, such as distance, time, or speed. A
* measure is <i>immutable</i> and <i>type safe</i>, making it easy to use in concurrent situations
* and gives compile-time safety. Two measures with the same <i>unit</i> and <i>magnitude</i> are
* effectively the same object.
*
* @param <U> the unit type of the measure
*/
public class ImmutableMeasure<U extends Unit<U>> implements Measure<U> {
private final double m_magnitude;
private final double m_baseUnitMagnitude;
private final U m_unit;
/**
* Creates a new immutable measure instance. This shouldn't be used directly; prefer one of the
* factory methods instead.
*
* @param magnitude the magnitude of this measure
* @param unit the unit of this measure.
*/
@SuppressWarnings("unchecked")
ImmutableMeasure(double magnitude, double baseUnitMagnitude, Unit<U> unit) {
Objects.requireNonNull(unit, "Unit cannot be null");
m_magnitude = magnitude;
m_baseUnitMagnitude = baseUnitMagnitude;
m_unit = (U) unit;
}
/**
* Creates a new measure in the given unit with a magnitude equal to the given one in base units.
*
* @param <U> the type of the units of measure
* @param baseUnitMagnitude the magnitude of the measure, in terms of the base unit of measure
* @param unit the unit of measure
* @return a new measure
*/
public static <U extends Unit<U>> ImmutableMeasure<U> ofBaseUnits(
double baseUnitMagnitude, Unit<U> unit) {
return new ImmutableMeasure<>(unit.fromBaseUnits(baseUnitMagnitude), baseUnitMagnitude, unit);
}
/**
* Creates a new measure in the given unit with a magnitude in terms of that unit.
*
* @param <U> the type of the units of measure
* @param relativeMagnitude the magnitude of the measure
* @param unit the unit of measure
* @return a new measure
*/
public static <U extends Unit<U>> ImmutableMeasure<U> ofRelativeUnits(
double relativeMagnitude, Unit<U> unit) {
return new ImmutableMeasure<>(relativeMagnitude, unit.toBaseUnits(relativeMagnitude), unit);
}
/** Gets the unitless magnitude of this measure. */
@Override
public double magnitude() {
return m_magnitude;
}
@Override
public double baseUnitMagnitude() {
return m_baseUnitMagnitude;
}
/** Gets the units of this measure. */
@Override
public U unit() {
return m_unit;
}
/**
* Checks for <i>object equality</i>. To check if two measures are <i>equivalent</i>, use {@link
* #isEquivalent(Measure) isEquivalent}.
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Measure)) {
return false;
}
Measure<?> that = (Measure<?>) o;
return Objects.equals(m_unit, that.unit()) && m_baseUnitMagnitude == that.baseUnitMagnitude();
}
@Override
public int hashCode() {
return Objects.hash(m_magnitude, m_unit);
}
@Override
public Measure<U> copy() {
return this; // already immutable, no need to allocate a new object
}
@Override
public String toString() {
return toShortString();
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Mass extends Unit<Mass> {
/** Creates a new unit with the given name and multiplier to the base unit. */
Mass(double baseUnitEquivalent, String name, String symbol) {
super(Mass.class, baseUnitEquivalent, name, symbol);
}
Mass(UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Mass.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,353 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
/**
* A measure holds the magnitude and unit of some dimension, such as distance, time, or speed. Two
* measures with the same <i>unit</i> and <i>magnitude</i> are effectively the same object.
*
* @param <U> the unit type of the measure
*/
public interface Measure<U extends Unit<U>> extends Comparable<Measure<U>> {
/**
* The threshold for two measures to be considered equivalent if converted to the same unit. This
* is only needed due to floating-point error.
*/
double EQUIVALENCE_THRESHOLD = 1e-12;
/** Gets the unitless magnitude of this measure. */
double magnitude();
/** Gets the magnitude of this measure in terms of the base unit. */
double baseUnitMagnitude();
/** Gets the units of this measure. */
U unit();
/**
* Converts this measure to a measure with a different unit of the same type, eg minutes to
* seconds. Converting to the same unit is equivalent to calling {@link #magnitude()}.
*
* <pre>
* Meters.of(12).in(Feet) // => 39.3701
* Seconds.of(15).in(Minutes) // => 0.25
* </pre>
*
* @param unit the unit to convert this measure to
* @return the value of this measure in the given unit
*/
default double in(Unit<U> unit) {
if (this.unit().equals(unit)) {
return magnitude();
} else {
return unit.fromBaseUnits(baseUnitMagnitude());
}
}
/**
* Multiplies this measurement by some constant multiplier and returns the result.
*
* @param multiplier the constant to multiply by
*/
default Measure<U> times(double multiplier) {
return ImmutableMeasure.ofBaseUnits(baseUnitMagnitude() * multiplier, unit());
}
/**
* Generates a new measure that is equal to this measure multiplied by another. Some dimensional
* analysis is performed to reduce the units down somewhat; for example, multiplying a {@code
* Measure<Time>} by a {@code Measure<Velocity<Distance>>} will return just a {@code
* Measure<Distance>} instead of the naive {@code Measure<Mult<Time, Velocity<Distance>>}. This is
* not guaranteed to perform perfect dimensional analysis.
*
* @param <U2> the type of the other measure to multiply by
* @param other the unit to multiply by
* @return the multiplicative unit
*/
@SuppressWarnings("unchecked")
default <U2 extends Unit<U2>> Measure<?> times(Measure<U2> other) {
if (other.unit() instanceof Dimensionless) {
// scalar multiplication
return times(other.baseUnitMagnitude());
}
if (unit() instanceof Per
&& other.unit().m_baseType.equals(((Per<?, ?>) unit()).denominator().m_baseType)) {
// denominator of the Per cancels out, return with just the units of the numerator
Unit<?> numerator = ((Per<?, ?>) unit()).numerator();
return numerator.ofBaseUnits(baseUnitMagnitude() * other.baseUnitMagnitude());
} else if (unit() instanceof Velocity && other.unit().m_baseType.equals(Time.class)) {
// Multiplying a velocity by a time, return the scalar unit (eg Distance)
Unit<?> numerator = ((Velocity<?>) unit()).getUnit();
return numerator.ofBaseUnits(baseUnitMagnitude() * other.baseUnitMagnitude());
} else if (other.unit() instanceof Per
&& unit().m_baseType.equals(((Per<?, ?>) other.unit()).denominator().m_baseType)) {
Unit<?> numerator = ((Per<?, ?>) other.unit()).numerator();
return numerator.ofBaseUnits(baseUnitMagnitude() * other.baseUnitMagnitude());
} else if (unit() instanceof Per
&& other.unit() instanceof Per
&& ((Per<?, ?>) unit())
.denominator()
.m_baseType
.equals(((Per<?, U>) other.unit()).numerator().m_baseType)
&& ((Per<?, ?>) unit())
.numerator()
.m_baseType
.equals(((Per<?, ?>) other.unit()).denominator().m_baseType)) {
// multiplying eg meters per second * milliseconds per foot
// return a scalar
return Units.Value.of(baseUnitMagnitude() * other.baseUnitMagnitude());
}
// Dimensional analysis fallthrough, do a basic unit multiplication
return unit().mult(other.unit()).ofBaseUnits(baseUnitMagnitude() * other.baseUnitMagnitude());
}
/**
* Divides this measurement by some constant divisor and returns the result. This is equivalent to
* {@code times(1 / divisor)}
*
* @param divisor the constant to divide by
* @see #times(double)
*/
default Measure<U> divide(double divisor) {
return times(1 / divisor);
}
/**
* Divides this measurement by some constant divisor and returns the result. This is equivalent to
* {@code divide(divisor.baseUnitMagnitude())}
*
* @param divisor the dimensionless measure to divide by
* @see #divide(double)
* @see #times(double)
*/
default Measure<U> divide(Measure<Dimensionless> divisor) {
return divide(divisor.baseUnitMagnitude());
}
/**
* Creates a velocity measure by dividing this one by a time period measure.
*
* <pre>
* Meters.of(1).per(Second) // => Measure&lt;Velocity&lt;Distance&gt;&gt;
* </pre>
*
* @param period the time period to divide by.
* @return the velocity result
*/
default Measure<Velocity<U>> per(Measure<Time> period) {
var newUnit = unit().per(period.unit());
return ImmutableMeasure.ofBaseUnits(baseUnitMagnitude() / period.baseUnitMagnitude(), newUnit);
}
/**
* Creates a relational measure equivalent to this one per some other unit.
*
* <pre>
* Volts.of(1.05).per(Meter) // => V/m, potential PID constant
* </pre>
*
* @param <U2> the type of the denominator unit
* @param denominator the denominator unit being divided by
* @return the relational measure
*/
default <U2 extends Unit<U2>> Measure<Per<U, U2>> per(U2 denominator) {
var newUnit = unit().per(denominator);
return newUnit.of(magnitude());
}
/**
* Adds another measure to this one. The resulting measure has the same unit as this one.
*
* @param other the measure to add to this one
* @return a new measure containing the result
*/
default Measure<U> plus(Measure<U> other) {
return unit().ofBaseUnits(baseUnitMagnitude() + other.baseUnitMagnitude());
}
/**
* Subtracts another measure from this one. The resulting measure has the same unit as this one.
*
* @param other the measure to subtract from this one
* @return a new measure containing the result
*/
default Measure<U> minus(Measure<U> other) {
return unit().ofBaseUnits(baseUnitMagnitude() - other.baseUnitMagnitude());
}
/** Negates this measure and returns the result. */
default Measure<U> negate() {
return times(-1);
}
/**
* Returns an immutable copy of this measure. The copy can be used freely and is guaranteed never
* to change.
*/
Measure<U> copy();
/**
* Creates a new mutable copy of this measure.
*
* @return a mutable measure initialized to be identical to this measure
*/
default MutableMeasure<U> mutableCopy() {
return MutableMeasure.mutable(this);
}
/**
* Checks if this measure is near another measure of the same unit. Provide a variance threshold
* for use for a +/- scalar, such as 0.05 for +/- 5%.
*
* <pre>
* Inches.of(11).isNear(Inches.of(10), 0.1) // => true
* Inches.of(12).isNear(Inches.of(10), 0.1) // => false
* </pre>
*
* @param other the other measurement to compare against
* @param varianceThreshold the acceptable variance threshold, in terms of an acceptable +/- error
* range multiplier. Checking if a value is within 10% means a value of 0.1 should be passed;
* checking if a value is within 1% means a value of 0.01 should be passed, and so on.
* @return true if this unit is near the other measure, otherwise false
*/
default boolean isNear(Measure<?> other, double varianceThreshold) {
if (this.unit().m_baseType != other.unit().m_baseType) {
return false; // Disjoint units, not compatible
}
// abs so negative inputs are calculated correctly
var allowedVariance = Math.abs(varianceThreshold);
return other.baseUnitMagnitude() * (1 - allowedVariance) <= this.baseUnitMagnitude()
&& other.baseUnitMagnitude() * (1 + allowedVariance) >= this.baseUnitMagnitude();
}
/**
* Checks if this measure is equivalent to another measure of the same unit.
*
* @param other the measure to compare to
* @return true if this measure is equivalent, false otherwise
*/
default boolean isEquivalent(Measure<?> other) {
if (this.unit().m_baseType != other.unit().m_baseType) {
return false; // Disjoint units, not compatible
}
return Math.abs(baseUnitMagnitude() - other.baseUnitMagnitude()) <= EQUIVALENCE_THRESHOLD;
}
@Override
default int compareTo(Measure<U> o) {
return Double.compare(this.baseUnitMagnitude(), o.baseUnitMagnitude());
}
/**
* Checks if this measure is greater than another measure of the same unit.
*
* @param o the other measure to compare to
*/
default boolean gt(Measure<U> o) {
return compareTo(o) > 0;
}
/**
* Checks if this measure is greater than or equivalent to another measure of the same unit.
*
* @param o the other measure to compare to
*/
default boolean gte(Measure<U> o) {
return compareTo(o) > 0 || isEquivalent(o);
}
/**
* Checks if this measure is less than another measure of the same unit.
*
* @param o the other measure to compare to
*/
default boolean lt(Measure<U> o) {
return compareTo(o) < 0;
}
/**
* Checks if this measure is less than or equivalent to another measure of the same unit.
*
* @param o the other measure to compare to
*/
default boolean lte(Measure<U> o) {
return compareTo(o) < 0 || isEquivalent(o);
}
/**
* Returns the measure with the absolute value closest to positive infinity.
*
* @param <U> the type of the units of the measures
* @param measures the set of measures to compare
* @return the measure with the greatest positive magnitude, or null if no measures were provided
*/
@SafeVarargs
static <U extends Unit<U>> Measure<U> max(Measure<U>... measures) {
if (measures.length == 0) {
return null; // nothing to compare
}
Measure<U> max = null;
for (Measure<U> measure : measures) {
if (max == null || measure.gt(max)) {
max = measure;
}
}
return max;
}
/**
* Returns the measure with the absolute value closest to negative infinity.
*
* @param <U> the type of the units of the measures
* @param measures the set of measures to compare
* @return the measure with the greatest negative magnitude
*/
@SafeVarargs
static <U extends Unit<U>> Measure<U> min(Measure<U>... measures) {
if (measures.length == 0) {
return null; // nothing to compare
}
Measure<U> max = null;
for (Measure<U> measure : measures) {
if (max == null || measure.lt(max)) {
max = measure;
}
}
return max;
}
/**
* Returns a string representation of this measurement in a shorthand form. The symbol of the
* backing unit is used, rather than the full name, and the magnitude is represented in scientific
* notation.
*
* @return the short form representation of this measurement
*/
default String toShortString() {
// eg 1.234e+04 V/m (1234 Volt per Meter in long form)
return String.format("%.3e %s", magnitude(), unit().symbol());
}
/**
* Returns a string representation of this measurement in a longhand form. The name of the backing
* unit is used, rather than its symbol, and the magnitude is represented in a full string, no
* scientific notation. (Very large values may be represented in scientific notation, however)
*
* @return the long form representation of this measurement
*/
default String toLongString() {
// eg 1234 Volt per Meter (1.234e+04 V/m in short form)
return String.format("%s %s", magnitude(), unit().name());
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import edu.wpi.first.units.collections.LongToObjectHashMap;
import java.util.Objects;
/**
* A combinatory unit type that is equivalent to the product of two other others. For example,
* Newton * Meters for torque could be represented as a unit of <code>
* Mult&lt;Force, Distance, Torque&gt;</code>
*
* @param <A> the type of the first unit in the result
* @param <B> the type of the second unit in the result
*/
public class Mult<A extends Unit<A>, B extends Unit<B>> extends Unit<Mult<A, B>> {
private final A m_unitA;
private final B m_unitB;
@SuppressWarnings("rawtypes")
private static final LongToObjectHashMap<Mult> cache = new LongToObjectHashMap<>();
protected Mult(Class<? extends Mult<A, B>> baseType, A a, B b) {
super(
baseType,
a.toBaseUnits(1) * b.toBaseUnits(1),
a.name() + "-" + b.name(),
a.symbol() + "*" + b.symbol());
m_unitA = a;
m_unitB = b;
}
/**
* Creates a new Mult unit derived from two arbitrary units multiplied together.
*
* <pre>
* Mult.combine(Volts, Meters) // => Volt-Meters
* </pre>
*
* <p>It's recommended to use the convenience function {@link Unit#mult(Unit)} instead of calling
* this factory directly.
*
* @param <A> the type of the first unit
* @param <B> the type of the second unit
* @param a the first unit
* @param b the second unit
* @return the combined unit
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static <A extends Unit<A>, B extends Unit<B>> Mult<A, B> combine(A a, B b) {
final long key = ((long) a.hashCode()) << 32L | ((long) b.hashCode()) & 0xFFFFFFFFL;
if (cache.containsKey(key)) {
return cache.get(key);
}
var mult = new Mult<A, B>((Class) Mult.class, a, b);
cache.put(key, mult);
return mult;
}
public A unitA() {
return m_unitA;
}
public B unitB() {
return m_unitB;
}
@Override
public String toString() {
return "(" + m_unitA.toString() + " * " + m_unitB.toString() + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
Mult<?, ?> mult = (Mult<?, ?>) o;
return Objects.equals(m_unitA, mult.m_unitA) && Objects.equals(m_unitB, mult.m_unitB);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), m_unitA, m_unitB);
}
}

View File

@@ -0,0 +1,302 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import java.util.Objects;
/**
* A specialization of {@link Measure} that allows for mutability. This is intended to be used for
* memory use reasons (such as on the memory-restricted RoboRIO 1 or 2 or SBC coprocessors) and
* should NOT be exposed in the public API for a class that uses it.
*
* <p>The advantage of using this class is that only one instance of a measurement object will exist
* at a time, as opposed to instantiating a new immutable instance every time a value is fetched.
* This can greatly reduce memory pressure, but comes at the cost of increased code complexity and
* sensitivity to race conditions if used poorly.
*
* <p>Any unsafe methods are prefixed with {@code mut_*}, such as {@link #mut_plus(Measure)} or
* {@link #mut_replace(Measure)}. These methods will change the internal state of the measurement
* object, and as such can be dangerous to use. They are primarily intended for use to track
* internal state of things like sensors
*
* @param <U> the type of the unit of measure
*/
public final class MutableMeasure<U extends Unit<U>> implements Measure<U> {
private double m_magnitude;
private double m_baseUnitMagnitude;
private U m_unit;
private MutableMeasure(double initialMagnitude, double baseUnitMagnitude, U unit) {
m_magnitude = initialMagnitude;
m_baseUnitMagnitude = baseUnitMagnitude;
m_unit = unit;
}
/**
* Creates a new mutable measure that is a copy of the given one.
*
* @param <U> the type of the units of measure
* @param measure the measure to create a mutable copy of
* @return a new mutable measure with an initial state equal to the given measure
*/
public static <U extends Unit<U>> MutableMeasure<U> mutable(Measure<U> measure) {
return new MutableMeasure<>(measure.magnitude(), measure.baseUnitMagnitude(), measure.unit());
}
/**
* Creates a new mutable measure with a magnitude of 0 in the given unit.
*
* @param <U> the type of the units of measure
* @param unit the unit of measure
* @return a new mutable measure
*/
public static <U extends Unit<U>> MutableMeasure<U> zero(U unit) {
return mutable(unit.zero());
}
/**
* Creates a new mutable measure in the given unit with a magnitude equal to the given one in base
* units.
*
* @param <U> the type of the units of measure
* @param baseUnitMagnitude the magnitude of the measure, in terms of the base unit of measure
* @param unit the unit of measure
* @return a new mutable measure
*/
public static <U extends Unit<U>> MutableMeasure<U> ofBaseUnits(
double baseUnitMagnitude, U unit) {
return new MutableMeasure<>(unit.fromBaseUnits(baseUnitMagnitude), baseUnitMagnitude, unit);
}
/**
* Creates a new mutable measure in the given unit with a magnitude in terms of that unit.
*
* @param <U> the type of the units of measure
* @param relativeMagnitude the magnitude of the measure
* @param unit the unit of measure
* @return a new mutable measure
*/
public static <U extends Unit<U>> MutableMeasure<U> ofRelativeUnits(
double relativeMagnitude, U unit) {
return new MutableMeasure<>(relativeMagnitude, unit.toBaseUnits(relativeMagnitude), unit);
}
@Override
public double magnitude() {
return m_magnitude;
}
@Override
public double baseUnitMagnitude() {
return m_baseUnitMagnitude;
}
@Override
public U unit() {
return m_unit;
}
// UNSAFE
/**
* Sets the new magnitude of the measurement. The magnitude must be in terms of the {@link
* #unit()}.
*
* @param magnitude the new magnitude of the measurement
*/
public void mut_setMagnitude(double magnitude) {
m_magnitude = magnitude;
m_baseUnitMagnitude = m_unit.toBaseUnits(magnitude);
}
/**
* Sets the new magnitude of the measurement. The magnitude must be in terms of the base unit of
* the current unit.
*
* @param baseUnitMagnitude the new magnitude of the measurement
*/
public void mut_setBaseUnitMagnitude(double baseUnitMagnitude) {
m_baseUnitMagnitude = baseUnitMagnitude;
m_magnitude = m_unit.fromBaseUnits(baseUnitMagnitude);
}
/**
* Overwrites the state of this measure and replaces it with values from the given one.
*
* @param other the other measure to copy values from
* @return this measure
*/
public MutableMeasure<U> mut_replace(Measure<U> other) {
m_magnitude = other.magnitude();
m_baseUnitMagnitude = other.baseUnitMagnitude();
m_unit = other.unit();
return this;
}
/**
* Overwrites the state of this measure with new values.
*
* @param magnitude the new magnitude in terms of the new unit
* @param unit the new unit
* @return this measure
*/
public MutableMeasure<U> mut_replace(double magnitude, U unit) {
this.m_magnitude = magnitude;
this.m_baseUnitMagnitude = unit.toBaseUnits(magnitude);
this.m_unit = unit;
return this;
}
/**
* Increments the current magnitude of the measure by the given value. The value must be in terms
* of the current {@link #unit() unit}.
*
* @param raw the raw value to accumulate by
* @return the measure
*/
public MutableMeasure<U> mut_acc(double raw) {
this.m_magnitude += raw;
this.m_baseUnitMagnitude += m_unit.toBaseUnits(raw);
return this;
}
/**
* Increments the current magnitude of the measure by the amount of the given measure.
*
* @param other the measure whose value should be added to this one
* @return the measure
*/
public MutableMeasure<U> mut_acc(Measure<U> other) {
m_baseUnitMagnitude += other.baseUnitMagnitude();
// can't naively use m_magnitude += other.in(m_unit) because the units may not
// be scalar multiples (eg adding 0C to 100K should result in 373.15K, not 100K)
m_magnitude = m_unit.fromBaseUnits(m_baseUnitMagnitude);
return this;
}
// Math
/**
* Adds another measurement to this one. This will mutate the object instead of generating a new
* measurement object.
*
* @param other the measurement to add
* @return this measure
*/
public MutableMeasure<U> mut_plus(Measure<U> other) {
return mut_plus(other.magnitude(), other.unit());
}
/**
* Adds another measurement to this one. This will mutate the object instead of generating a new
* measurement object. This is a denormalized version of {@link #mut_plus(Measure)} to avoid
* having to wrap raw numbers in a {@code Measure} object and pay for an object allocation.
*
* @param magnitude the magnitude of the other measurement.
* @param unit the unit of the other measurement
* @return this measure
*/
public MutableMeasure<U> mut_plus(double magnitude, U unit) {
mut_setBaseUnitMagnitude(m_baseUnitMagnitude + unit.toBaseUnits(magnitude));
return this;
}
/**
* Subtracts another measurement to this one. This will mutate the object instead of generating a
* new measurement object.
*
* @param other the measurement to add
* @return this measure
*/
public MutableMeasure<U> mut_minus(Measure<U> other) {
return mut_minus(other.magnitude(), other.unit());
}
/**
* Subtracts another measurement to this one. This will mutate the object instead of generating a
* new measurement object. This is a denormalized version of {@link #mut_minus(Measure)} to avoid
* having to wrap raw numbers in a {@code Measure} object and pay for an object allocation.
*
* @param magnitude the magnitude of the other measurement.
* @return this measure
*/
public MutableMeasure<U> mut_minus(double magnitude, U unit) {
return mut_plus(-magnitude, unit);
}
/**
* Multiplies this measurement by some constant value. This will mutate the object instead of
* generating a new measurement object.
*
* @param multiplier the multiplier to scale the measurement by
* @return this measure
*/
public MutableMeasure<U> mut_times(double multiplier) {
mut_setBaseUnitMagnitude(m_baseUnitMagnitude * multiplier);
return this;
}
/**
* Multiplies this measurement by some constant value. This will mutate the object instead of
* generating a new measurement object.
*
* @param multiplier the multiplier to scale the measurement by
* @return this measure
*/
public MutableMeasure<U> mut_times(Measure<? extends Dimensionless> multiplier) {
return mut_times(multiplier.baseUnitMagnitude());
}
/**
* Divides this measurement by some constant value. This will mutate the object instead of
* generating a new measurement object.
*
* @param divisor the divisor to scale the measurement by
* @return this measure
*/
public MutableMeasure<U> mut_divide(double divisor) {
mut_setBaseUnitMagnitude(m_baseUnitMagnitude / divisor);
return this;
}
/**
* Divides this measurement by some constant value. This will mutate the object instead of
* generating a new measurement object.
*
* @param divisor the divisor to scale the measurement by
* @return this measure
*/
public MutableMeasure<U> mut_divide(Measure<? extends Dimensionless> divisor) {
return mut_divide(divisor.baseUnitMagnitude());
}
@Override
public Measure<U> copy() {
return new ImmutableMeasure<>(m_magnitude, m_baseUnitMagnitude, m_unit);
}
@Override
public String toString() {
return toShortString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Measure)) {
return false;
}
Measure<?> that = (Measure<?>) o;
return Objects.equals(m_unit, that.unit()) && this.isEquivalent(that);
}
@Override
public int hashCode() {
return Objects.hash(m_magnitude, m_unit);
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import edu.wpi.first.units.collections.LongToObjectHashMap;
import java.util.Objects;
/**
* Generic combinatory unit type that represents the proportion of one unit to another, such as
* Meters per Second or Radians per Celsius.
*
* <p>Note that due to restrictions with the Java type system, velocities (change per unit time) are
* represented by the {@link Velocity} class. Accelerations are represented by {@code
* Velocity<Velocity<X>>}, and so on.
*
* @param <N> the type of the numerator unit
* @param <D> the type of the denominator unit
*/
public class Per<N extends Unit<N>, D extends Unit<D>> extends Unit<Per<N, D>> {
private final N m_numerator;
private final D m_denominator;
/**
* Keep a cache of created instances so expressions like Volts.per(Meter) don't do any allocations
* after the first.
*/
@SuppressWarnings("rawtypes")
private static final LongToObjectHashMap<Per> cache = new LongToObjectHashMap<>();
protected Per(Class<Per<N, D>> baseType, N numerator, D denominator) {
super(
baseType,
numerator.toBaseUnits(1) / denominator.toBaseUnits(1),
numerator.name() + " per " + denominator.name(),
numerator.symbol() + "/" + denominator.symbol());
m_numerator = numerator;
m_denominator = denominator;
}
/**
* Creates a new Per unit derived from an arbitrary numerator and time denominator units. Using a
* denominator with a unit of time is discouraged; use {@link Velocity} instead.
*
* <pre>
* Per.combine(Volts, Meters) // => possible PID constant
* </pre>
*
* <p>It's recommended to use the convenience function {@link Unit#per(Unit)} instead of calling
* this factory directly.
*
* @param <N> the type of the numerator unit
* @param <D> the type of the denominator unit
* @param numerator the numerator unit
* @param denominator the denominator for unit time
* @return the combined unit
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static <N extends Unit<N>, D extends Unit<D>> Per<N, D> combine(
N numerator, D denominator) {
final long key =
((long) numerator.hashCode()) << 32L | ((long) denominator.hashCode()) & 0xFFFFFFFFL;
var existing = cache.get(key);
if (existing != null) {
return existing;
}
var newUnit = new Per<N, D>((Class) Per.class, numerator, denominator);
cache.put(key, newUnit);
return newUnit;
}
public N numerator() {
return m_numerator;
}
public D denominator() {
return m_denominator;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
Per<?, ?> per = (Per<?, ?>) o;
return Objects.equals(m_numerator, per.m_numerator)
&& Objects.equals(m_denominator, per.m_denominator);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), m_numerator, m_denominator);
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Power extends Unit<Power> {
Power(double baseUnitEquivalent, String name, String symbol) {
super(Power.class, baseUnitEquivalent, name, symbol);
}
Power(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Power.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Temperature extends Unit<Temperature> {
Temperature(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Temperature.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Time extends Unit<Time> {
/** Creates a new unit with the given name and multiplier to the base unit. */
Time(double baseUnitEquivalent, String name, String symbol) {
super(Time.class, baseUnitEquivalent, name, symbol);
}
Time(UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Time.class, toBaseConverter, fromBaseConverter, name, symbol);
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import java.util.Objects;
@FunctionalInterface
public interface UnaryFunction {
double apply(double input);
/**
* Constructs a new function that first calls this function, then passes the result to another as
* input.
*
* <pre>
* f = x -> x + 1 // f(x) = x + 1
* g = x -> 2 * x // g(x) = 2x
*
* h = f.pipeTo(g) // h(x) = g(f(x))
* </pre>
*
* @param next the next operation to pipe to
*/
default UnaryFunction pipeTo(UnaryFunction next) {
Objects.requireNonNull(next, "The next operation in the chain must be provided");
return x -> next.apply(this.apply(x));
}
/** h(x) = f(x) * g(x). */
default UnaryFunction mult(UnaryFunction multiplier) {
Objects.requireNonNull(multiplier, "A multiplier function must be provided");
return x -> this.apply(x) * multiplier.apply(x);
}
/** g(x) = f(x) * k. */
default UnaryFunction mult(double multiplier) {
return x -> this.apply(x) * multiplier;
}
/** h(x) = f(x) / g(x). */
default UnaryFunction div(UnaryFunction divisor) {
Objects.requireNonNull(divisor, "A divisor function must be provided");
return x -> {
double numerator = this.apply(x);
// fast-track to avoid another function call
// avoids returning NaN if divisor is also zero
if (numerator == 0) {
return 0;
}
double div = divisor.apply(x);
return numerator / div; // NOTE: returns +Infinity or -Infinity if div is zero
};
}
/** g(x) * f(x) / k. */
default UnaryFunction div(double divisor) {
return x -> this.apply(x) / divisor;
}
/** h(x) = f(x) ^ g(x). */
default UnaryFunction exp(UnaryFunction exponent) {
Objects.requireNonNull(exponent, "An exponent function must be provided");
return x -> Math.pow(this.apply(x), exponent.apply(x));
}
default UnaryFunction exp(double exponent) {
return x -> Math.pow(this.apply(x), exponent);
}
}

View File

@@ -0,0 +1,296 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import java.util.Objects;
/**
* A unit is some unit of measurement that defines a quantity, such as grams, meters, or seconds.
*
* @param <U> the self type, e.g. {@code class SomeUnit extends Unit<SomeUnit>}
*/
public class Unit<U extends Unit<U>> {
private final UnaryFunction m_toBaseConverter;
private final UnaryFunction m_fromBaseConverter;
final Class<? extends U> m_baseType; // package-private for the builder
private Measure<U> m_zero;
private Measure<U> m_one;
private final String m_name;
private final String m_symbol;
/**
* Creates a new unit defined by its relationship to some base unit.
*
* @param baseType the base type of the unit, e.g. Distance.class for the distance unit
* @param toBaseConverter a function for converting units of this type to the base unit
* @param fromBaseConverter a function for converting units of the base unit to this one
* @param name the name of the unit. This should be a singular noun (so "Meter", not "Meters")
* @param symbol the short symbol for the unit, such as "m" for meters or "lb." for pounds
*/
protected Unit(
Class<? extends U> baseType,
UnaryFunction toBaseConverter,
UnaryFunction fromBaseConverter,
String name,
String symbol) {
m_baseType = Objects.requireNonNull(baseType);
m_toBaseConverter = Objects.requireNonNull(toBaseConverter);
m_fromBaseConverter = Objects.requireNonNull(fromBaseConverter);
m_name = Objects.requireNonNull(name);
m_symbol = Objects.requireNonNull(symbol);
}
/**
* Creates a new unit with the given name and multiplier to the base unit.
*
* @param baseType the base type of the unit, e.g. Distance.class for the distance unit
* @param baseUnitEquivalent the multiplier to convert this unit to the base unit of this type.
* For example, meters has a multiplier of 1, mm has a multiplier of 1e3, and km has
* multiplier of 1e-3.
* @param name the name of the unit. This should be a singular noun (so "Meter", not "Meters")
* @param symbol the short symbol for the unit, such as "m" for meters or "lb." for pounds
*/
protected Unit(
Class<? extends U> baseType, double baseUnitEquivalent, String name, String symbol) {
this(baseType, x -> x * baseUnitEquivalent, x -> x / baseUnitEquivalent, name, symbol);
}
/**
* Converts a value in terms of base units to a value in terms of this unit.
*
* @param valueInBaseUnits the value in base units to convert
* @return the equivalent value in terms of this unit
*/
public double fromBaseUnits(double valueInBaseUnits) {
return m_fromBaseConverter.apply(valueInBaseUnits);
}
/**
* Converts a value in terms of this unit to a value in terms of the base unit.
*
* @param valueInNativeUnits the value in terms of this unit to convert
* @return the equivalent value in terms of the base unit
*/
public double toBaseUnits(double valueInNativeUnits) {
return m_toBaseConverter.apply(valueInNativeUnits);
}
/**
* Converts a magnitude in terms of another unit of the same dimension to a magnitude in terms of
* this unit.
*
* <pre>
* Inches.convertFrom(12, Feet) // => 144.0
* Kilograms.convertFrom(2.2, Pounds) // => 0.9979024
* </pre>
*
* @param magnitude a magnitude measured in another unit
* @param otherUnit the unit to convert the magnitude to
*/
public double convertFrom(double magnitude, Unit<U> otherUnit) {
if (this.equivalent(otherUnit)) {
// same unit, don't bother converting
return magnitude;
}
return this.fromBaseUnits(otherUnit.toBaseUnits(magnitude));
}
/**
* Gets the conversion function used to convert values to base unit terms. This generally
* shouldn't need to be used directly; prefer {@link #toBaseUnits(double)} instead.
*
* @return the conversion function
*/
public UnaryFunction getConverterToBase() {
return m_toBaseConverter;
}
/**
* Gets the conversion function used to convert values to terms of this unit. This generally
* shouldn't need to be used directly; prefer {@link #fromBaseUnits(double)} instead.
*
* @return the conversion function
*/
public UnaryFunction getConverterFromBase() {
return m_fromBaseConverter;
}
/**
* Creates a new measure of this unit with the given value. The resulting measure is
* <i>immutable</i> and cannot have its value modified.
*
* @param magnitude the magnitude of the measure to create
*/
public Measure<U> of(double magnitude) {
if (magnitude == 0) {
// reuse static object
return zero();
}
if (magnitude == 1) {
// reuse static object
return one();
}
return ImmutableMeasure.ofRelativeUnits(magnitude, this);
}
/**
* Creates a new measure with a magnitude equal to the given base unit magnitude, converted to be
* in terms of this unit.
*
* @param baseUnitMagnitude the magnitude of the measure in terms of the base unit
* @return the measure
*/
public Measure<U> ofBaseUnits(double baseUnitMagnitude) {
return ImmutableMeasure.ofBaseUnits(baseUnitMagnitude, this);
}
/**
* Gets a measure with a magnitude of 0 in terms of this unit.
*
* @return the zero-valued measure
*/
public Measure<U> zero() {
// lazy init because 'this' is null in object initialization
if (m_zero == null) {
m_zero = ImmutableMeasure.ofRelativeUnits(0, this);
}
return m_zero;
}
/**
* Gets a measure with a magnitude of 1 in terms of this unit.
*
* @return the 1-valued measure
*/
public Measure<U> one() {
// lazy init because 'this' is null in object initialization
if (m_one == null) {
m_one = ImmutableMeasure.ofRelativeUnits(1, this);
}
return m_one;
}
/**
* Creates a velocity unit derived from this one. Can be chained to denote velocity, acceleration,
* jerk, etc.
*
* <pre>
* Meters.per(Second) // => linear velocity
* Kilograms.per(Second) // => mass flow
* Feet.per(Second).per(Second).of(32) // roughly 1G of acceleration
* </pre>
*
* @param period the time period of the velocity, such as seconds or milliseconds
* @return a velocity unit corresponding to the rate of change of this unit over time
*/
public Velocity<U> per(Time period) {
return Velocity.combine(this, period);
}
/**
* Takes this unit and creates a new proportional unit where this unit is the numerator and the
* given denominator is the denominator.
*
* <pre>
* Volts.per(Meter) // V/m
* </pre>
*
* @param <D> the type of the denominator units
* @param denominator the denominator of the proportional unit
* @return a combined proportional unit
*/
@SuppressWarnings("unchecked")
public <D extends Unit<D>> Per<U, D> per(D denominator) {
return Per.combine((U) this, denominator);
}
/**
* Takes this unit and creates a new combinatory unit equivalent to this unit multiplied by
* another.
*
* <pre>
* Volts.mult(Meter) // V*m
* </pre>
*
* @param <U2> the type of the unit to multiply by
* @param other the unit to multiply by
* @return a combined unit equivalent to this unit multiplied by the other
*/
@SuppressWarnings("unchecked")
public <U2 extends Unit<U2>> Mult<U, U2> mult(U2 other) {
return Mult.combine((U) this, other);
}
/**
* Checks if this unit is equivalent to another one. Equivalence is determined by both units
* having the same base type and treat the same base unit magnitude as the same magnitude in their
* own units, to within {@link Measure#EQUIVALENCE_THRESHOLD}.
*
* @param other the unit to compare to.
* @return true if both units are equivalent, false if not
*/
public boolean equivalent(Unit<?> other) {
if (this.m_baseType != other.m_baseType) {
// different unit types, not compatible
return false;
}
double arbitrary = 16_777.214; // 2^24 / 1e3
return Math.abs(
this.m_fromBaseConverter.apply(arbitrary)
- other.m_fromBaseConverter.apply(arbitrary))
<= Measure.EQUIVALENCE_THRESHOLD
&& Math.abs(
this.m_toBaseConverter.apply(arbitrary) - other.m_toBaseConverter.apply(arbitrary))
<= Measure.EQUIVALENCE_THRESHOLD;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Unit)) {
return false;
}
Unit<?> that = (Unit<?>) o;
return m_baseType.equals(that.m_baseType)
&& m_name.equals(that.m_name)
&& m_symbol.equals(that.m_symbol)
&& this.equivalent(that);
}
@Override
public int hashCode() {
return Objects.hash(m_toBaseConverter, m_fromBaseConverter, m_baseType, m_name, m_symbol);
}
/**
* Gets the name of this unit.
*
* @return the unit's name
*/
public String name() {
return m_name;
}
/**
* Gets the symbol of this unit.
*
* @return the unit's symbol
*/
public String symbol() {
return m_symbol;
}
@Override
public String toString() {
return name();
}
}

View File

@@ -0,0 +1,216 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
/**
* Builder used for easily deriving new units from existing ones.
*
* @param <U> the type of the unit
*/
public final class UnitBuilder<U extends Unit<U>> {
private final U m_base;
private UnaryFunction m_fromBase;
private UnaryFunction m_toBase;
private String m_name;
private String m_symbol;
public UnitBuilder(U base) {
this.m_base = Objects.requireNonNull(base, "Base unit cannot be null");
}
/**
* Sets the unit conversions based on a simple offset. The new unit will have its values equal to
* (base value - offset).
*
* @param offset the offset
*/
public UnitBuilder<U> offset(double offset) {
m_toBase = derivedValue -> derivedValue + offset;
m_fromBase = baseValue -> baseValue - offset;
return this;
}
/**
* Maps a value {@code value} in the range {@code [inMin..inMax]} to an output in the range {@code
* [outMin..outMax]}. Inputs outside the bounds will be mapped correspondingly to outputs outside
* the output bounds. Inputs equal to {@code inMin} will be mapped to {@code outMin}, and inputs
* equal to {@code inMax} will similarly be mapped to {@code outMax}.
*
* @param value the value to map
* @param inMin the minimum input value (does not have to be absolute)
* @param inMax the maximum input value (does not have to be absolute)
* @param outMin the minimum output value (does not have to be absolute)
* @param outMax the maximum output value (does not have to be absolute)
* @return the mapped output
*/
// NOTE: This method lives here instead of in MappingBuilder because inner classes can't
// define static methods prior to Java 16.
private static double mapValue(
double value, double inMin, double inMax, double outMin, double outMax) {
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
/** Helper class used for safely chaining mapping builder calls. */
public class MappingBuilder {
private final double m_minInput;
private final double m_maxInput;
private MappingBuilder(double minInput, double maxInput) {
this.m_minInput = minInput;
this.m_maxInput = maxInput;
}
/**
* Finalizes the mapping by defining the output range.
*
* @param minOutput the minimum output value (does not have to be absolute)
* @param maxOutput the maximum output value (does not have to be absolute)
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> toOutputRange(double minOutput, double maxOutput) {
UnitBuilder.this.m_fromBase = x -> mapValue(x, m_minInput, m_maxInput, minOutput, maxOutput);
UnitBuilder.this.m_toBase = y -> mapValue(y, minOutput, maxOutput, m_minInput, m_maxInput);
return UnitBuilder.this;
}
}
/**
* Defines a mapping for values within the given input range. This method call should be
* immediately followed by {@code .toOutputRange}, eg {@code mappingInputRange(1,
* 2).toOutputRange(3, 4)}, which will return the unit builder for continued chaining.
*
* @param minBase the minimum input value (does not have to be absolute)
* @param maxBase the maximum output value (does not have to be absolute)
* @return a builder object used to define the output range
*/
public MappingBuilder mappingInputRange(double minBase, double maxBase) {
return new MappingBuilder(minBase, maxBase);
}
/**
* Sets the conversion function to transform values in the base unit to values in the derived
* unit.
*
* @param fromBase the conversion function
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> fromBase(UnaryFunction fromBase) {
this.m_fromBase = Objects.requireNonNull(fromBase, "fromBase function cannot be null");
return this;
}
/**
* Sets the conversion function to transform values in the derived unit to values in the base
* unit.
*
* @param toBase the conversion function
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> toBase(UnaryFunction toBase) {
this.m_toBase = Objects.requireNonNull(toBase, "toBase function cannot be null");
return this;
}
/**
* Sets the name of the new unit.
*
* @param name the new name
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> named(String name) {
this.m_name = name;
return this;
}
/**
* Sets the symbol of the new unit.
*
* @param symbol the new symbol
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> symbol(String symbol) {
this.m_symbol = symbol;
return this;
}
/**
* Helper for defining units that are a scalar fraction of the base unit, such as centimeters
* being 1/100th of the base unit (meters). The fraction value is specified as the denominator of
* the fraction, so a centimeter definition would use {@code splitInto(100)} instead of {@code
* splitInto(1/100.0)}.
*
* @param fraction the denominator portion of the fraction of the base unit that a value of 1 in
* the derived unit corresponds to
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> splitInto(double fraction) {
if (fraction == 0) {
throw new IllegalArgumentException("Fraction must be nonzero");
}
return toBase(x -> x / fraction).fromBase(b -> b * fraction);
}
/**
* Helper for defining units that are a scalar multiple of the base unit, such as kilometers being
* 1000x of the base unit (meters).
*
* @param aggregation the magnitude required for a measure in the base unit to equal a magnitude
* of 1 in the derived unit
* @return the unit builder, for continued chaining
*/
public UnitBuilder<U> aggregate(double aggregation) {
if (aggregation == 0) {
throw new IllegalArgumentException("Aggregation amount must be nonzero");
}
return toBase(x -> x * aggregation).fromBase(b -> b / aggregation);
}
/**
* Creates the new unit based off of the builder methods called prior.
*
* @return the new derived unit
* @throws NullPointerException if the unit conversions, unit name, or unit symbol were not set
* @throws RuntimeException if the base unit does not define a constructor accepting the
* conversion functions, unit name, and unit symbol - in that order
*/
@SuppressWarnings("PMD.AvoidAccessibilityAlteration")
public U make() {
Objects.requireNonNull(m_fromBase, "fromBase function was not set");
Objects.requireNonNull(m_toBase, "toBase function was not set");
Objects.requireNonNull(m_name, "new unit name was not set");
Objects.requireNonNull(m_symbol, "new unit symbol was not set");
Class<? extends U> baseType = m_base.m_baseType;
try {
Constructor<? extends U> ctor =
baseType.getDeclaredConstructor(
UnaryFunction.class, // toBaseUnits
UnaryFunction.class, // fromBaseUnits
String.class, // name
String.class); // symbol
// need to flag the constructor as accessible so we can use private, package-private, and
// protected constructors
ctor.setAccessible(true);
return ctor.newInstance(
m_toBase.pipeTo(m_base.getConverterToBase()),
m_base.getConverterFromBase().pipeTo(m_fromBase),
m_name,
m_symbol);
} catch (InstantiationException e) {
throw new RuntimeException("Could not instantiate class " + baseType.getName(), e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Could not access constructor", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Constructing " + baseType.getName() + " raised an exception", e);
} catch (NoSuchMethodException e) {
throw new RuntimeException("No compatible constructor", e);
}
}
}

View File

@@ -0,0 +1,207 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import java.util.Locale;
public final class Units {
private Units() {
// Prevent instantiation
}
// Pseudo-classes describing the more common units of measure.
@SuppressWarnings("rawtypes")
public static final Unit AnonymousBaseUnit = new Dimensionless(1, "<?>", "<?>");
// Distance
public static final Distance Meters = BaseUnits.Distance;
public static final Distance Millimeters = Milli(Meters, "Millimeter", "mm");
public static final Distance Centimeters =
derive(Meters).splitInto(100).named("Centimeter").symbol("cm").make();
public static final Distance Inches =
derive(Millimeters).aggregate(25.4).named("Inch").symbol("in").make();
public static final Distance Feet =
derive(Inches).aggregate(12).named("Foot").symbol("ft").make();
// Time
public static final Time Seconds = BaseUnits.Time;
public static final Time Second = Seconds; // singularized alias
public static final Time Milliseconds = Milli(Seconds);
public static final Time Millisecond = Milliseconds; // singularized alias
public static final Time Microseconds = Micro(Seconds);
public static final Time Microsecond = Microseconds; // singularized alias
public static final Time Minutes =
derive(Seconds).aggregate(60).named("Minute").symbol("min").make();
public static final Time Minute = Minutes; // singularized alias
// Angle
public static final Angle Revolutions = BaseUnits.Angle;
public static final Angle Rotations = new Angle(1, "Rotation", "R"); // alias
public static final Angle Radians =
derive(Revolutions).splitInto(2 * Math.PI).named("Radian").symbol("rad").make();
public static final Angle Degrees =
derive(Revolutions).splitInto(360).named("Degree").symbol("°").make();
// Velocity
public static final Velocity<Distance> MetersPerSecond = Meters.per(Second);
public static final Velocity<Distance> FeetPerSecond = Feet.per(Second);
public static final Velocity<Distance> InchesPerSecond = Inches.per(Second);
public static final Velocity<Angle> RevolutionsPerSecond = Revolutions.per(Second);
public static final Velocity<Angle> RotationsPerSecond = Rotations.per(Second);
public static final Velocity<Angle> RPM = Rotations.per(Minute);
public static final Velocity<Angle> RadiansPerSecond = Radians.per(Second);
public static final Velocity<Angle> DegreesPerSecond = Degrees.per(Second);
// Acceleration
public static final Velocity<Velocity<Distance>> MetersPerSecondPerSecond =
MetersPerSecond.per(Second);
public static final Velocity<Velocity<Distance>> Gs =
derive(MetersPerSecondPerSecond).aggregate(9.80665).named("G").symbol("G").make();
// Mass
public static final Mass Kilograms = BaseUnits.Mass;
public static final Mass Grams = Milli(Kilograms, "Gram", "g");
public static final Mass Pounds =
derive(Grams).aggregate(453.592).named("Pound").symbol("lb.").make();
public static final Mass Ounces =
derive(Pounds).splitInto(16).named("Ounce").symbol("oz.").make();
// Unitless
public static final Dimensionless Value = BaseUnits.Value;
public static final Dimensionless Percent =
derive(Value).splitInto(100).named("Percent").symbol("%").make();
// Voltage
public static final Voltage Volts = BaseUnits.Voltage;
public static final Voltage Millivolts = Milli(Volts);
// Current
public static final Current Amps = BaseUnits.Current;
public static final Current Milliamps = Milli(Amps);
// Energy
public static final Energy Joules = BaseUnits.Energy;
public static final Energy Millijoules = Milli(Joules);
public static final Energy Kilojoules = Kilo(Joules);
// Power
public static final Power Watts = BaseUnits.Power;
public static final Power Milliwatts = Milli(Watts);
public static final Power Horsepower =
derive(Watts).aggregate(745.7).named("Horsepower").symbol("HP").make();
// Temperature
public static final Temperature Kelvin = BaseUnits.Temperature;
public static final Temperature Celsius =
derive(Kelvin).offset(+273.15).named("Celsius").symbol("°C").make();
public static final Temperature Fahrenheit =
derive(Celsius)
.mappingInputRange(0, 100)
.toOutputRange(32, 212)
.named("Fahrenheit")
.symbol("°F")
.make();
// Standard feedforward units for kV and kA.
// kS and kG are just volts, which is already defined earlier
public static final Per<Voltage, Velocity<Distance>> VoltsPerMeterPerSecond =
Volts.per(MetersPerSecond);
public static final Per<Voltage, Velocity<Velocity<Distance>>> VoltsPerMeterPerSecondSquared =
Volts.per(MetersPerSecondPerSecond);
public static final Per<Voltage, Velocity<Angle>> VoltsPerRadianPerSecond =
Volts.per(RadiansPerSecond);
public static final Per<Voltage, Velocity<Velocity<Angle>>> VoltsPerRadianPerSecondSquared =
Volts.per(RadiansPerSecond.per(Second));
/**
* Creates a unit equal to a thousandth of the base unit, eg Milliseconds = Milli(Units.Seconds).
*
* @param <U> the type of the unit
* @param baseUnit the unit being derived from. This does not have to be the base unit of measure
* @param name the name of the new derived unit
* @param symbol the symbol of the new derived unit
*/
@SuppressWarnings({"PMD.MethodName", "checkstyle:methodname"})
public static <U extends Unit<U>> U Milli(Unit<U> baseUnit, String name, String symbol) {
return derive(baseUnit).splitInto(1000).named(name).symbol(symbol).make();
}
/**
* Creates a unit equal to a thousandth of the base unit, eg Milliseconds = Milli(Units.Seconds).
*
* @param <U> the type of the unit
* @param baseUnit the unit being derived from. This does not have to be the base unit of measure
*/
@SuppressWarnings({"PMD.MethodName", "checkstyle:methodname"})
public static <U extends Unit<U>> U Milli(Unit<U> baseUnit) {
return Milli(
baseUnit, "Milli" + baseUnit.name().toLowerCase(Locale.ROOT), "m" + baseUnit.symbol());
}
/**
* Creates a unit equal to a millionth of the base unit, eg {@code Microseconds =
* Micro(Units.Seconds, "Microseconds", 'us")}.
*
* @param <U> the type of the unit
* @param baseUnit the unit being derived from. This does not have to be the base unit of measure
* @param name the name of the new derived unit
* @param symbol the symbol of the new derived unit
*/
@SuppressWarnings({"PMD.MethodName", "checkstyle:methodname"})
public static <U extends Unit<U>> U Micro(Unit<U> baseUnit, String name, String symbol) {
return derive(baseUnit).splitInto(1_000_000).named(name).symbol(symbol).make();
}
/**
* Creates a unit equal to a millionth of the base unit, eg Microseconds = Micro(Units.Seconds).
*
* @param <U> the type of the unit
* @param baseUnit the unit being derived from. This does not have to be the base unit of measure
*/
@SuppressWarnings({"PMD.MethodName", "checkstyle:methodname"})
public static <U extends Unit<U>> U Micro(Unit<U> baseUnit) {
return Micro(
baseUnit, "Micro" + baseUnit.name().toLowerCase(Locale.ROOT), "u" + baseUnit.symbol());
}
/**
* Creates a unit equal to a thousand of the base unit, eg Kilograms = Kilo(Units.Grams).
*
* @param <U> the type of the unit
* @param baseUnit the unit being derived from. This does not have to be the base unit of measure
* @param name the name of the new derived unit
* @param symbol the symbol of the new derived unit
*/
@SuppressWarnings({"PMD.MethodName", "checkstyle:methodname"})
public static <U extends Unit<U>> U Kilo(Unit<U> baseUnit, String name, String symbol) {
return derive(baseUnit).aggregate(1000).named(name).symbol(symbol).make();
}
/**
* Creates a unit equal to a thousand of the base unit, eg Kilograms = Kilo(Units.Grams).
*
* @param <U> the type of the unit
* @param baseUnit the unit being derived from. This does not have to be the base unit of measure
*/
@SuppressWarnings({"PMD.MethodName", "checkstyle:methodname"})
public static <U extends Unit<U>> U Kilo(Unit<U> baseUnit) {
return Kilo(
baseUnit, "Kilo" + baseUnit.name().toLowerCase(Locale.ROOT), "K" + baseUnit.symbol());
}
@SuppressWarnings("unchecked")
public static <U extends Unit<U>> UnitBuilder<U> derive(Unit<U> unit) {
return new UnitBuilder<>((U) unit);
}
@SuppressWarnings("unchecked")
public static <U extends Unit<U>> U anonymous() {
return (U) AnonymousBaseUnit;
}
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import edu.wpi.first.units.collections.LongToObjectHashMap;
import java.util.Objects;
public class Velocity<D extends Unit<D>> extends Unit<Velocity<D>> {
private final D m_unit;
private final Time m_period;
/**
* Stores velocity units that were created ad-hoc using {@link #combine(Unit, Time, String,
* String)}. Does not store objects created directly by constructors.
*/
@SuppressWarnings("rawtypes")
private static final LongToObjectHashMap<Velocity> cache = new LongToObjectHashMap<>();
/** Generates a cache key used for cache lookups. */
private static long cacheKey(Unit<?> numerator, Unit<?> denominator) {
return ((long) numerator.hashCode()) << 32L | ((long) denominator.hashCode()) & 0xFFFFFFFFL;
}
/**
* Creates a new velocity unit derived from an arbitrary numerator and time period units.
*
* <p>Results of this method are cached so future invocations with the same arguments will return
* the pre-existing units instead of generating new identical ones.
*
* <pre>
* Velocity.combine(Kilograms, Second) // => mass flow
* Velocity.combine(Feet, Millisecond) // => linear speed
* Velocity.combine(Radians, Second) // => angular speed
*
* Velocity.combine(Feet.per(Second), Second) // => linear acceleration in ft/s/s
* Velocity.combine(Radians.per(Second), Second) // => angular acceleration
* </pre>
*
* <p>It's recommended to use the convenience function {@link Unit#per(Time)} instead of calling
* this factory directly.
*
* @param <D> the type of the numerator unit
* @param numerator the numerator unit
* @param period the period for unit time
* @param name the name of the new velocity unit
* @param symbol the symbol of the new velocity unit
* @return the new unit
*/
@SuppressWarnings("unchecked")
public static <D extends Unit<D>> Velocity<D> combine(
Unit<D> numerator, Time period, String name, String symbol) {
long key = cacheKey(numerator, period);
if (cache.containsKey(key)) {
return cache.get(key);
}
Velocity<D> velocity = new Velocity<>((D) numerator, period, name, symbol);
cache.put(key, velocity);
return velocity;
}
/**
* Creates a new velocity unit derived from an arbitrary numerator and time period units.
*
* <p>Results of this method are cached so future invocations with the same arguments will return
* the pre-existing units instead of generating new identical ones.
*
* <p>This method automatically generates a new name and symbol for the new velocity unit.
*
* <pre>
* Velocity.combine(Kilograms, Second) // => mass flow
* Velocity.combine(Feet, Millisecond) // => linear speed
* Velocity.combine(Radians, Second) // => angular speed
*
* Velocity.combine(Feet.per(Second), Second) // => linear acceleration in ft/s/s
* Velocity.combine(Radians.per(Second), Second) // => angular acceleration
* </pre>
*
* <p>It's recommended to use the convenience function {@link Unit#per(Time)} instead of calling
* this factory directly.
*
* @param <D> the type of the numerator unit
* @param numerator the numerator unit
* @param period the period for unit time
* @return the new unit
*/
@SuppressWarnings("unchecked")
public static <D extends Unit<D>> Velocity<D> combine(Unit<D> numerator, Time period) {
long key = cacheKey(numerator, period);
if (cache.containsKey(key)) {
return cache.get(key);
}
var name = numerator.name() + " per " + period.name();
var symbol = numerator.symbol() + "/" + period.symbol();
Velocity<D> velocity = new Velocity<>((D) numerator, period, name, symbol);
cache.put(key, velocity);
return velocity;
}
@SuppressWarnings({"unchecked", "rawtypes"})
Velocity(D unit, Time period, String name, String symbol) {
super((Class) Velocity.class, unit.toBaseUnits(1) / period.toBaseUnits(1), name, symbol);
this.m_unit = unit;
this.m_period = period;
}
@SuppressWarnings({"unchecked", "rawtypes"})
Velocity(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super((Class) Velocity.class, toBaseConverter, fromBaseConverter, name, symbol);
this.m_unit = Units.anonymous();
this.m_period = Units.Seconds;
}
/**
* Gets the major unit being measured (eg Meters for Meters per Second).
*
* @return the major unit
*/
public D getUnit() {
return m_unit;
}
/**
* Gets the period unit of the velocity, eg Seconds or Milliseconds.
*
* @return the period unit
*/
public Time getPeriod() {
return m_period;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
Velocity<?> velocity = (Velocity<?>) o;
return m_unit.equals(velocity.m_unit) && m_period.equals(velocity.m_period);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), m_unit, m_period);
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
public class Voltage extends Unit<Voltage> {
Voltage(double baseUnitEquivalent, String name, String symbol) {
super(Voltage.class, baseUnitEquivalent, name, symbol);
}
Voltage(
UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) {
super(Voltage.class, toBaseConverter, fromBaseConverter, name, symbol);
}
public Power times(Unit<Current> current, String name, String symbol) {
return new Power(toBaseUnits(1) * current.toBaseUnits(1), name, symbol);
}
}

View File

@@ -0,0 +1,332 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units.collections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* A variant on {@code java.util.HashMap<K, V>} that uses primitive long ints for map keys instead
* of autoboxed Long objects like would be used for a {@code Map<Long, V>}.
*
* @param <V> the type of the values stored in the map
*/
public class LongToObjectHashMap<V> {
private static final int kInitialSize = 0;
private static final int kInitialCapacity = 8; // NOTE: must be a power of two
/**
* The default load factor of the hashmap. If the ratio of the number of entries to the map's
* capacity exceeds this value, the map will be resized (doubled capacity) in order for more
* values to be easily inserted.
*/
private static final double kLoadFactor = 75.00 / 100;
/** The current number of key-value pairs in the map. */
private int m_size = kInitialSize;
/**
* The current maximum capacity of the map. Note that it will be resized before m_size reaches
* this value.
*/
private int m_capacity = kInitialCapacity;
/**
* The keys in the map. This is a sparse array, and the location of a key may not be equal to the
* result of calling {@link #bucket(long)} on that key. To handle hash collisions, if a bucket is
* already in use when trying to insert a value, the bucket number is incremented (wrapping around
* to 0 if it's equal to m_capacity) and <i>that</i> bucket is checked to see if it's available.
* This process continues until an empty bucket is found (which is guaranteed because m_size is
* always less than m_capacity).
*/
private long[] m_keys = new long[m_capacity];
/** Tracks which buckets are actually used (have a key-value mapping). */
private boolean[] m_uses = new boolean[m_capacity];
/**
* The values in the map. See the documentation for m_keys for how indexing into this array works.
*/
@SuppressWarnings("unchecked")
private V[] m_values = (V[]) new Object[m_capacity];
/**
* Puts a value {@code value} corresponding to key {@code key} in the map.
*
* @param key the associated key
* @param value the value to insert
* @return the previous value that was mapped to the key, or null if no such value existed
*/
public V put(long key, V value) {
int bucket = bucket(key);
// Increment the bucket until we hit an open space (there's always going to be at least one)
while (m_uses[bucket]) {
if (m_keys[bucket] == key) {
// replace the existing value
var oldValue = m_values[bucket];
m_values[bucket] = value;
return oldValue;
}
bucket = safeIncrement(bucket);
}
m_uses[bucket] = true;
m_keys[bucket] = key;
m_values[bucket] = value;
m_size++;
if (m_size > maxSize()) {
grow();
}
return null;
}
/**
* Gets the value associated with the given key.
*
* @param key the key to retrieve the value for
* @return the value mapped to the key, or null if the key is not in the map
*/
public V get(long key) {
int bucket = bucket(key);
while (m_uses[bucket]) {
if (m_keys[bucket] == key) {
// found it
return m_values[bucket];
}
bucket = safeIncrement(bucket);
}
return null;
}
/**
* Removes the value associated with the given key and returns it.
*
* @param key the key to remove
* @return the value corresponding to the key, or null if the key is not in the map
*/
public V remove(long key) {
int bucket = bucket(key);
while (m_uses[bucket]) {
if (m_keys[bucket] == key) {
// found it
// TODO: Shrink the map when below a certain load factor
// Current use cases don't remove elements from the map, so there's not much use
// for shrinking at the moment.
m_size--;
m_keys[bucket] = 0L;
m_uses[bucket] = false;
var oldValue = m_values[bucket];
m_values[bucket] = null;
return oldValue;
}
bucket = safeIncrement(bucket);
}
return null;
}
/**
* Checks if a key is contained in the map.
*
* @param key the key to check
* @return true if the key has an associated value, false if not
*/
public boolean containsKey(long key) {
int bucket = bucket(key);
while (m_uses[bucket]) {
if (m_keys[bucket] == key) {
return true;
}
bucket = safeIncrement(bucket);
}
return false;
}
/** Clears and removes all entries from the map. */
public void clear() {
if (m_size == 0) {
// Nothing to do
return;
}
m_size = 0;
Arrays.fill(m_uses, false);
Arrays.fill(m_keys, 0L);
Arrays.fill(m_values, null);
}
/**
* Gets the number of key-value pairs currently contained in the map.
*
* @return the current size of the map
*/
public int size() {
return m_size;
}
// package-private for tests
int capacity() {
return m_capacity;
}
/**
* Checks if the map contains any entries.
*
* @return true if at least one entry is present, false otherwise
*/
public boolean isEmpty() {
return m_size == 0;
}
/**
* Gets the keys contained in the map. Ordering is not guaranteed. The returned set is read-only
* and immutable. This uses a custom class for primitive long values to avoid unnecessary
* autoboxing to {@code java.lang.Long}.
*
* @return a read-only set of keys
*/
public ReadOnlyPrimitiveLongSet keySet() {
// copy the sparse key array into a compact array
final long[] keys = new long[m_size];
int i = 0;
for (int bucket = 0; bucket < m_capacity; bucket++) {
if (m_uses[bucket]) {
keys[i] = m_keys[bucket];
i++;
}
}
return new ReadOnlyPrimitiveLongSet(keys);
}
/**
* Gets the values contained in the map. Ordering is not guaranteed. The returned collection is
* read-only and immutable.
*
* @return a read-only collection of values
*/
public Collection<V> values() {
Collection<V> values = new ArrayList<>();
for (int bucket = 0; bucket < m_capacity; bucket++) {
if (m_uses[bucket]) {
values.add(m_values[bucket]);
}
}
return List.copyOf(values); // return a readonly copy
}
@FunctionalInterface
public interface IteratorFunction<V> {
void accept(long key, V value);
}
/**
* Iterates over every key-value pair in the map and passes them to the given function.
*
* @param function the function to apply to every key-value pair.
*/
public void forEach(IteratorFunction<? super V> function) {
for (int bucket = 0; bucket < m_capacity; bucket++) {
if (m_uses[bucket]) {
function.accept(m_keys[bucket], m_values[bucket]);
}
}
}
private void grow() {
final int currentSize = m_size;
final int oldCapacity = m_capacity;
if (oldCapacity * kLoadFactor >= currentSize) {
// We're below the maximum allowed size for the current capacity
// Nothing to do
return;
}
final int newCapacity = oldCapacity * 2;
final int newMask = newCapacity - 1;
final boolean[] oldUses = m_uses;
final long[] oldKeys = m_keys;
final V[] oldValues = m_values;
final boolean[] newUses = new boolean[newCapacity];
final long[] newKeys = new long[newCapacity];
@SuppressWarnings("unchecked")
final V[] newValues = (V[]) new Object[newCapacity];
for (int oldBucket = 0; oldBucket < oldCapacity; oldBucket++) {
if (!oldUses[oldBucket]) {
// Bucket is empty, skip
continue;
}
final long key = oldKeys[oldBucket];
final V value = oldValues[oldBucket];
int newBucket = (int) (hash(key) & newMask);
while (newUses[newBucket]) {
newBucket = (newBucket + 1) & newMask;
}
newUses[newBucket] = true;
newKeys[newBucket] = key;
newValues[newBucket] = value;
}
m_capacity = newCapacity;
m_uses = newUses;
m_keys = newKeys;
m_values = newValues;
}
private int maxSize() {
return (int) (m_capacity * kLoadFactor);
}
/**
* Calculates a hashcode for an input key. Does some bit shuffling to account for poor hash
* functions.
*
* @param key the key to hash
* @return a hashcode for the input key
*/
private long hash(long key) {
return 31 + (key ^ (key >>> 15) ^ (key >>> 31) ^ (key << 31));
}
/**
* The mask to use when translating a hashcode to a bucket index. Relies on m_capacity being a
* power of two.
*/
private int mask() {
return m_capacity - 1;
}
/**
* Calculates the desired bucket index for a particular key. Does nothing to handle the case where
* the calculated index is already in use by another key.
*
* @param key the key to get the bucket for
* @return the desired bucket index
*/
private int bucket(long key) {
var hash = hash(key);
return (int) (hash & mask());
}
/**
* Increments a bucket index by 1, wrapping around to 0 if the index is already at the maximum.
*
* @param bucket the index to increment
* @return the incremented bucket index
*/
private int safeIncrement(int bucket) {
return (bucket + 1) & mask();
}
}

View File

@@ -0,0 +1,125 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units.collections;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.stream.LongStream;
/** A read-only set of unique primitive {@code long} values. */
public class ReadOnlyPrimitiveLongSet implements Iterable<Long> {
private final long[] m_values;
/**
* Creates a new set from the given values. These values do not have to be unique.
*
* @param values the values that belong to the set.
*/
@SuppressWarnings({"PMD.ForLoopCanBeForeach", "ForLoopReplaceableByForEach"})
public ReadOnlyPrimitiveLongSet(long... values) {
// initial size is the upper limit
long[] uniqueValues = new long[values.length];
int numUniqueValues = 0;
boolean seenZero = false;
// copy the set of unique values to our array
// using indexed for-loops to avoid allocations
copyLoop:
for (int i = 0; i < values.length; i++) {
long value = values[i];
if (value == 0 && !seenZero) {
// special case to support zero
seenZero = true;
} else {
for (int j = 0; j < uniqueValues.length; j++) {
long uniqueValue = uniqueValues[j];
if (uniqueValue == value) {
continue copyLoop;
}
}
}
uniqueValues[numUniqueValues] = value;
numUniqueValues++;
}
if (numUniqueValues == values.length) {
// all input values were unique, no need to truncate
m_values = uniqueValues;
} else {
// truncate the array to remove trailing empty space
m_values = Arrays.copyOf(uniqueValues, numUniqueValues);
}
}
/**
* Checks if the set contains a particular value.
*
* @param value the value to check for
* @return true if the value is in the set, false if not
*/
public boolean contains(long value) {
for (long mValue : m_values) {
if (mValue == value) {
return true;
}
}
return false;
}
/**
* Retrieves the number of elements in the set.
*
* @return the number of elements in the set
*/
public int size() {
return m_values.length;
}
/**
* Checks if the set is empty, i.e. contains no values.
*
* @return true if there are no values in the set, false otherwise.
*/
public boolean isEmpty() {
return size() == 0;
}
/** Creates a stream of primitive long values for the set. */
public LongStream stream() {
return Arrays.stream(m_values);
}
/**
* Creates a new array that contains all of the values in the set.
*
* @return an array containing all the values in the set
*/
public long[] toArray() {
return Arrays.copyOf(m_values, m_values.length);
}
@Override
public Iterator<Long> iterator() {
return new Iterator<>() {
@SuppressWarnings("PMD.RedundantFieldInitializer")
private int m_index = 0;
@Override
public boolean hasNext() {
return m_index < ReadOnlyPrimitiveLongSet.this.size();
}
@Override
public Long next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return ReadOnlyPrimitiveLongSet.this.m_values[m_index++];
}
};
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class CurrentTest {
@Test
void testAmpsTimesVolts() {
Power combined = Units.Amps.times(Units.Volts, "Watt", "w");
assertTrue(combined.equivalent(Units.Watts));
}
@Test
void testMilliAmpsTimesMilliVolts() {
// results in microwatts
assertTrue(
Units.Milliamps.times(Units.Millivolts, "Microwatt", "uW")
.equivalent(Units.Milli(Units.Milliwatts)));
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class DistanceTest {
@Test
void testBaseUnitDistancePerTime() {
Velocity<Distance> anonBaseUnit = new Distance(1, "D", "d").per(new Time(1, "T", "t"));
assertTrue(BaseUnits.Velocity.equivalent(anonBaseUnit));
}
@Test
void testFeetPerSecond() {
Velocity<Distance> feetPerMillisecond = Units.Feet.per(Units.Milliseconds);
// one foot per millisecond
// = (1 / 3.28084) meters per (1 / 1000) seconds
// = (1000 / 3.28084) meters per second
double asBaseMeasure = feetPerMillisecond.of(1).in(Units.MetersPerSecond);
assertEquals(1000 / 3.28084, asBaseMeasure, 1e-3);
// one meter per second = 1 mm per millisecond = 0.00328084 feet per millisecond
double asContrivedMeasure = Units.MetersPerSecond.of(1).in(feetPerMillisecond);
assertEquals(3.28084 / 1000, asContrivedMeasure, 1e-8);
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static edu.wpi.first.units.Units.Inches;
import static edu.wpi.first.units.Units.Revolutions;
import static edu.wpi.first.units.Units.Second;
import static edu.wpi.first.units.Units.Value;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class EncoderTest {
static class Encoder<U extends Unit<U>> {
int m_ticks; // = 0
private Measure<U> m_distancePerPulse;
private MutableMeasure<U> m_distance;
private MutableMeasure<Velocity<U>> m_rate;
void setDistancePerPulse(Measure<U> distancePerPulse) {
m_distancePerPulse = distancePerPulse;
m_distance = MutableMeasure.zero(distancePerPulse.unit());
m_rate = MutableMeasure.zero(distancePerPulse.unit().per(Second));
}
Measure<U> getDistance() {
return m_distance;
}
Measure<Velocity<U>> getRate() {
return m_rate;
}
void setTicks(int ticks) {
// pretend we read from JNI here instead of being passed a specific value
var change = ticks - m_ticks;
m_ticks = ticks;
m_distance.mut_setMagnitude(m_distancePerPulse.magnitude() * ticks);
// assumes the last update was 1 second ago - fine for tests
m_rate.mut_setMagnitude(m_distancePerPulse.magnitude() * change);
}
}
@Test
void testAsDistance() {
var ticksPerRevolution = Value.of(2048);
var encoder = new Encoder<Distance>();
// distance per rotation = (wheel circumference / gear ratio)
// distance per tick = distance per rotation / ticks per rotation
var wheelDiameter = Inches.of(6);
var gearRatio = Value.of(10); // 10:1 ratio
Measure<Distance> distancePerPulse =
wheelDiameter.times(Math.PI).divide(gearRatio).divide(ticksPerRevolution);
encoder.setDistancePerPulse(distancePerPulse);
encoder.m_ticks = 0;
assertEquals(0, encoder.getDistance().in(Inches), Measure.EQUIVALENCE_THRESHOLD);
assertEquals(0, encoder.getRate().in(Inches.per(Second)), Measure.EQUIVALENCE_THRESHOLD);
// one full encoder turn, 1/10th of a wheel rotation
encoder.setTicks(2048);
assertEquals(6 * Math.PI / 10, encoder.getDistance().in(Inches), Measure.EQUIVALENCE_THRESHOLD);
// one full encoder turn back, 1/10th of a wheel rotation - rate should be negative
encoder.setTicks(0);
assertEquals(
-6 * Math.PI / 10, encoder.getRate().in(Inches.per(Second)), Measure.EQUIVALENCE_THRESHOLD);
}
@Test
void testAsRevolutions() {
var ticksPerRevolution = Value.of(2048);
var encoder = new Encoder<Angle>();
Measure<Angle> distancePerPulse = Revolutions.of(1).divide(ticksPerRevolution);
encoder.setDistancePerPulse(distancePerPulse);
encoder.m_ticks = 0;
assertEquals(0, encoder.getDistance().in(Revolutions), Measure.EQUIVALENCE_THRESHOLD);
assertEquals(0, encoder.getRate().in(Revolutions.per(Second)), Measure.EQUIVALENCE_THRESHOLD);
encoder.setTicks(2048); // one full encoder turn, 1/10th of a wheel rotation
assertEquals(1, encoder.getDistance().in(Revolutions), Measure.EQUIVALENCE_THRESHOLD);
assertEquals(1, encoder.getRate().in(Revolutions.per(Second)), Measure.EQUIVALENCE_THRESHOLD);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
class ExampleUnit extends Unit<ExampleUnit> {
ExampleUnit(double baseUnitEquivalent) {
this(baseUnitEquivalent, "Example", "ex");
}
ExampleUnit(UnaryFunction toBase, UnaryFunction fromBase, String name, String symbol) {
super(ExampleUnit.class, toBase, fromBase, name, symbol);
}
ExampleUnit(double baseUnitEquivalent, String name, String symbol) {
super(ExampleUnit.class, baseUnitEquivalent, name, symbol);
}
}

View File

@@ -0,0 +1,312 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class MeasureTest {
@Test
void testBasics() {
Unit<Distance> unit = Units.Feet;
double magnitude = 10;
Measure<Distance> m = unit.of(magnitude);
assertEquals(unit, m.unit(), "Wrong units");
assertEquals(magnitude, m.magnitude(), 0, "Wrong magnitude");
}
@Test
void testMultiply() {
Measure<Distance> m = Units.Feet.of(1);
Measure<Distance> m2 = m.times(10);
assertEquals(10, m2.magnitude(), 1e-12);
assertNotSame(m2, m); // make sure state wasn't changed
}
@Test
void testDivide() {
Measure<Distance> m = Units.Meters.of(1);
Measure<Distance> m2 = m.divide(10);
assertEquals(0.1, m2.magnitude(), 0);
assertNotSame(m2, m);
}
@Test
void testAdd() {
Measure<Distance> m1 = Units.Feet.of(1);
Measure<Distance> m2 = Units.Inches.of(2);
assertTrue(m1.plus(m2).isEquivalent(Units.Feet.of(1 + 2 / 12d)));
assertTrue(m2.plus(m1).isEquivalent(Units.Inches.of(14)));
}
@Test
void testSubtract() {
Measure<Distance> m1 = Units.Feet.of(1);
Measure<Distance> m2 = Units.Inches.of(2);
assertTrue(m1.minus(m2).isEquivalent(Units.Feet.of(1 - 2 / 12d)));
assertTrue(m2.minus(m1).isEquivalent(Units.Inches.of(-10)));
}
@Test
void testNegate() {
Measure<Distance> m = Units.Feet.of(123);
Measure<Distance> n = m.negate();
assertEquals(-m.magnitude(), n.magnitude(), 1e-12);
assertEquals(m.unit(), n.unit());
}
@Test
void testEquivalency() {
Measure<Distance> inches = Units.Inches.of(12);
Measure<Distance> feet = Units.Feet.of(1);
assertTrue(inches.isEquivalent(feet));
assertTrue(feet.isEquivalent(inches));
}
@Test
void testAs() {
Measure<Distance> m = Units.Inches.of(12);
assertEquals(1, m.in(Units.Feet), Measure.EQUIVALENCE_THRESHOLD);
}
@Test
void testPerUnitTime() {
var measure = Units.Kilograms.of(144);
var dt = Units.Milliseconds.of(53);
// 144 Kg / (53 ms) = (1000 / 53) * 144 Kg/s = (144,000 / 53) Kg/s
var result = measure.per(dt);
assertEquals(144_000.0 / 53, result.baseUnitMagnitude(), 1e-5);
assertEquals(Units.Kilograms.per(Units.Milliseconds), result.unit());
}
@Test
void testTimesMeasure() {
var m1 = Units.Volts.of(1.567);
var m2 = Units.Kilograms.of(8.4e-5);
assertEquals(Units.Volts.mult(Units.Kilograms).of(1.567 * 8.4e-5), m1.times(m2));
}
@Test
void testTimesUnitless() {
var unit = new ExampleUnit(6);
var measure = unit.of(2.5);
var multiplier = Units.Percent.of(125); // 125% or 1.25x
Measure<?> result = measure.times(multiplier);
assertSame(unit, result.unit());
assertEquals(2.5 * 1.25, result.magnitude());
assertEquals(2.5 * 1.25 * 6, result.baseUnitMagnitude());
}
@Test
void testTimesPerWithDimensionalAnalysis() {
var measureA = Units.Feet.of(62); // 62 feet
var measureB = Units.Radians.of(6).per(Units.Inches); // 6 radians per inch
Measure<?> aTimesB = measureA.times(measureB); // (62 feet) * (6 rad/inch) = 4464 rad
assertEquals(Units.Radians, aTimesB.unit());
assertEquals(4464, aTimesB.magnitude(), 1e-12);
Measure<?> bTimesA = measureB.times(measureA); // should be identical to the above
assertTrue(bTimesA.isEquivalent(aTimesB));
assertTrue(aTimesB.isEquivalent(bTimesA));
}
@Test
void testPerTimesPerWithDimensionalAnalysis() {
var measureA = Units.Inches.of(16).per(Units.Volts);
var measureB = Units.Millivolts.of(14).per(Units.Meters);
Measure<?> aTimesB = measureA.times(measureB);
assertEquals(Units.Value, aTimesB.unit());
assertEquals((16 * 25.4 / 1000) * (14 / 1000.0), aTimesB.magnitude());
assertEquals((16 * 25.4 / 1000) * (14 / 1000.0), aTimesB.baseUnitMagnitude());
Measure<?> bTimesA = measureB.times(measureA); // should be identical to the above
assertTrue(bTimesA.isEquivalent(aTimesB));
assertTrue(aTimesB.isEquivalent(bTimesA));
}
@Test
void testPerTimesMeasure() {
var m1 = Units.Feet.per(Units.Milliseconds).of(19);
var m2 = Units.Seconds.of(44);
// 19 ft/ms = 19,000 ft/s
// 19,000 ft/s * 44s = 836,000 ft
assertTrue(Units.Feet.of(836_000).isNear(m1.times(m2), 1e-12));
// 42 ex per foot * 17mm = 42 ex * 17mm / (304.8mm/ft) = 42 * 17 / 304.8 = 2.34252
var exampleUnit = new ExampleUnit(1);
var m3 = exampleUnit.per(Units.Feet).of(42);
var m4 = Units.Millimeters.of(17);
assertEquals(exampleUnit.of(42 * 17 / (12 * 25.4)), m3.times(m4));
}
@Test
void testToShortString() {
var measure = Units.Volts.of(343);
assertEquals("3.430e+02 V", measure.toShortString());
}
@Test
void testToLongString() {
var measure = Units.Volts.of(343);
assertEquals("343.0 Volt", measure.toLongString());
assertEquals("343.0001 Volt", Units.Volts.of(343.0001).toLongString());
assertEquals("1.2345678912345679E8 Volt", Units.Volts.of(123456789.123456789).toLongString());
}
@Test
void testOfBaseUnits() {
var unit = new ExampleUnit(16);
var measure = unit.ofBaseUnits(1);
assertEquals(unit, measure.unit());
assertEquals(1, measure.baseUnitMagnitude());
assertEquals(1 / 16.0, measure.magnitude());
}
@Test
void testCompare() {
var unit = new ExampleUnit(7);
var base = unit.of(1);
var less = unit.of(0.5);
var more = unit.of(2);
assertEquals(0, base.compareTo(base));
assertEquals(-1, base.compareTo(more));
assertEquals(1, base.compareTo(less));
// lt, lte, gt, gte helper functions
assertTrue(base.lt(more));
assertTrue(base.lte(more));
assertFalse(base.gt(more));
assertFalse(base.gte(more));
assertTrue(base.gt(less));
assertTrue(base.gte(less));
assertFalse(base.lt(less));
assertFalse(base.lte(less));
assertTrue(base.lte(base));
assertTrue(base.gte(base));
assertFalse(base.lt(base));
assertFalse(base.gt(base));
}
@Test
void testTimesScalar() {
var unit = new ExampleUnit(42);
var measure = unit.of(4.2);
var scalar = 18;
var result = measure.times(scalar);
assertNotSame(measure, result);
assertSame(unit, result.unit());
assertEquals(4.2 * 18, result.magnitude());
assertEquals(4.2 * 42 * 18, result.baseUnitMagnitude());
}
@Test
void testPerUnit() {
var unitA = new ExampleUnit(10);
var unitB = new ExampleUnit(12);
var measure = unitA.of(1.2);
var result = measure.per(unitB);
assertEquals(Per.combine(unitA, unitB), result.unit()); // A/B has base equivalent of 10/12
assertEquals(1, result.baseUnitMagnitude()); // 10/12 * 12/10 = 1
assertEquals(measure.magnitude(), result.magnitude());
}
@Test
void testAddMeasureSameUnit() {
var unit = new ExampleUnit(8.2);
var measureA = unit.of(3.1);
var measureB = unit.of(91.6);
var result = measureA.plus(measureB);
assertEquals(unit, result.unit());
assertEquals(94.7, result.magnitude(), 1e-12);
}
@Test
void testAddMeasuresDifferentUnits() {
var unitA = new ExampleUnit(8.2);
var unitB = new ExampleUnit(7.3);
var measureA = unitA.of(5);
var measureB = unitB.of(16);
var aPlusB = measureA.plus(measureB);
assertEquals(unitA, aPlusB.unit());
assertEquals(8.2 * 5 + 7.3 * 16, aPlusB.baseUnitMagnitude(), 1e-12);
assertEquals(5 + (16 * 7.3 / 8.2), aPlusB.magnitude(), 1e-12);
var bPlusA = measureB.plus(measureA);
assertEquals(unitB, bPlusA.unit());
assertEquals(8.2 * 5 + 7.3 * 16, bPlusA.baseUnitMagnitude(), 1e-12);
assertEquals(16 + (5 * 8.2 / 7.3), bPlusA.magnitude(), 1e-12);
}
@Test
void testMinNoArgs() {
var min = Measure.min();
assertNull(min);
}
@Test
void testMin() {
var unit = new ExampleUnit(56.1);
var one = unit.of(1);
var two = unit.of(2);
var zero = unit.of(0);
var veryNegative = unit.of(-12839712);
var min = Measure.min(one, two, zero, veryNegative);
assertSame(veryNegative, min);
}
@Test
void testMaxNoArgs() {
var min = Measure.max();
assertNull(min);
}
@Test
void testMax() {
var unit = new ExampleUnit(6.551);
var one = unit.of(1);
var two = unit.of(2);
var zero = unit.of(0);
var veryLarge = unit.of(8217234);
var max = Measure.max(one, two, zero, veryLarge);
assertSame(veryLarge, max);
}
@Test
void testIsNear() {
var unit = new ExampleUnit(92);
var measureA = unit.of(1.21);
var measureB = unit.ofBaseUnits(64);
// A = 1.21 * 92 base units, or 111.32
// B = 64 base units
// ratio = 111.32 / 64 = 1.739375 = 173.9375%
assertTrue(measureA.isNear(measureA, 0));
assertTrue(measureB.isNear(measureB, 0));
assertFalse(measureA.isNear(measureB, 0));
assertFalse(measureA.isNear(measureB, 0.50));
assertFalse(measureA.isNear(measureB, 0.739370));
assertTrue(measureA.isNear(measureB, 0.739375));
assertTrue(measureA.isNear(measureB, 100)); // some stupidly large range +/- 10000%
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import org.junit.jupiter.api.Test;
class MultTest {
@Test
void testAutomaticNames() {
var unitA = new ExampleUnit(1, "Ay", "a");
var unitB = new ExampleUnit(1, "Bee", "b");
var mult = Mult.combine(unitA, unitB);
assertEquals("Ay-Bee", mult.name());
assertEquals("a*b", mult.symbol());
}
@Test
void testCombine() {
var unitA = new ExampleUnit(100);
var unitB = new ExampleUnit(0.912);
var mult = Mult.combine(unitA, unitB);
assertEquals(91.2, mult.toBaseUnits(1));
}
@Test
void testCaches() {
var unitA = new ExampleUnit(1);
var unitB = new ExampleUnit(2);
var mult = Mult.combine(unitA, unitB);
assertSame(mult, Mult.combine(unitA, unitB));
}
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static edu.wpi.first.units.Units.Centimeters;
import static edu.wpi.first.units.Units.FeetPerSecond;
import static edu.wpi.first.units.Units.InchesPerSecond;
import static edu.wpi.first.units.Units.Meters;
import static edu.wpi.first.units.Units.Millisecond;
import static edu.wpi.first.units.Units.Millivolts;
import static edu.wpi.first.units.Units.Second;
import static edu.wpi.first.units.Units.Volts;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import org.junit.jupiter.api.Test;
class MutableMeasureTest {
@Test
void testWrapper() {
var unit = new ExampleUnit(1);
var measure = unit.of(1234);
var mutable = MutableMeasure.mutable(measure);
assertEquals(measure.magnitude(), mutable.magnitude(), 0);
assertEquals(measure.unit(), mutable.unit());
}
@Test
void testSetMagnitude() {
var measure = MutableMeasure.ofRelativeUnits(0, FeetPerSecond);
double newMagnitude = 48.123;
measure.mut_setMagnitude(newMagnitude);
assertEquals(newMagnitude, measure.magnitude(), 0); // should be an exact match
double feetToMeters = 0.3048;
assertEquals(newMagnitude * feetToMeters, measure.baseUnitMagnitude(), 1e-12);
}
@Test
void testMut_times() {
var measure = MutableMeasure.ofRelativeUnits(18, FeetPerSecond);
double scalar = Math.PI;
var result = measure.mut_times(scalar);
assertSame(measure, result, "mut_times should return the same object");
assertEquals(18 * Math.PI, measure.magnitude(), 1e-12);
}
@Test
void testMut_divide() {
var measure = MutableMeasure.ofRelativeUnits(18, FeetPerSecond);
double scalar = Math.PI;
var result = measure.mut_divide(scalar);
assertSame(measure, result, "mut_times should return the same object");
assertEquals(18 / Math.PI, measure.magnitude(), 1e-12);
}
@Test
void testReplaceMeasure() {
var measure = MutableMeasure.ofRelativeUnits(17.6, FeetPerSecond);
var replacer = Meters.per(Millisecond).of(94.872);
var result = measure.mut_replace(replacer);
assertSame(measure, result, "Replacing should return the mutable measure");
assertSame(replacer.unit(), measure.unit());
assertEquals(replacer.magnitude(), measure.magnitude(), 0);
}
@Test
void testReplaceRaw() {
var measure = MutableMeasure.ofRelativeUnits(-542, FeetPerSecond);
var result = measure.mut_replace(62, Meters.per(Millisecond));
assertSame(measure, result, "Replacing should return the mutable measure");
assertSame(Meters.per(Millisecond), measure.unit());
assertEquals(62, measure.magnitude(), 0);
}
@Test
void testAccMeasure() {
var measure = MutableMeasure.ofRelativeUnits(8.5431, FeetPerSecond);
var acc = Meters.per(Millisecond).of(-23.62);
var result = measure.mut_acc(acc);
assertSame(measure, result, "Acc should return the mutable measure");
assertSame(FeetPerSecond, measure.unit(), "Unit shouldn't change");
assertEquals(8.5431 - (23.62 * (1 / 0.3048) * 1000), measure.magnitude(), 1e-10);
}
@Test
void testAccRaw() {
var measure = MutableMeasure.ofRelativeUnits(99.999999, FeetPerSecond);
var result = measure.mut_acc(22.981);
assertSame(measure, result);
assertSame(FeetPerSecond, measure.unit());
assertEquals(122.980999, measure.magnitude(), 0);
}
@Test
void testMutPlusMeasure() {
var measure = MutableMeasure.ofRelativeUnits(400, InchesPerSecond);
var other = Centimeters.per(Second).of(41.312);
var result = measure.mut_plus(other);
assertSame(measure, result);
assertSame(InchesPerSecond, result.unit());
assertEquals(416.2645669291339, measure.magnitude(), 1e-12);
}
@Test
void testMutPlusRaw() {
var measure = MutableMeasure.ofRelativeUnits(31.51, Volts);
var result = measure.mut_plus(66.641, Millivolts);
assertSame(measure, result);
assertSame(Volts, result.unit());
assertEquals(31.576641, result.magnitude(), 1e-10);
}
@Test
void testMutMinusMeasure() {
var measure = MutableMeasure.ofRelativeUnits(400, InchesPerSecond);
var other = Centimeters.per(Second).of(41.312);
var result = measure.mut_minus(other);
assertSame(measure, result);
assertSame(InchesPerSecond, result.unit());
assertEquals(383.7354330708662, measure.magnitude(), 1e-12);
}
@Test
void testMutMinusRaw() {
var measure = MutableMeasure.ofRelativeUnits(31.51, Volts);
var result = measure.mut_minus(66.641, Millivolts);
assertSame(measure, result);
assertSame(Volts, result.unit());
assertEquals(31.443359, result.magnitude(), 1e-10);
}
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class UnaryFunctionTest {
@Test
void testApply() {
UnaryFunction f = x -> x * x;
assertEquals(1, f.apply(1));
assertEquals(4, f.apply(2));
assertEquals(2, f.apply(1.414), 1e-3);
assertEquals(64, f.apply(-8));
}
@Test
void testPipeTo() {
UnaryFunction f = x -> x * x;
UnaryFunction g = x -> x + 1;
var h = f.pipeTo(g); // h(x) = g(f(x)) = x^2 + 1
assertEquals(2, h.apply(1));
assertEquals(65, h.apply(8));
}
@Test
void testMult() {
UnaryFunction f = x -> x * x;
UnaryFunction g = x -> x + 1;
var h = f.mult(g); // h(x) = f(x) * g(x) = (x^2)(x + 1) = x^3 + x^2
assertEquals(2, h.apply(1));
assertEquals(216 + 36, h.apply(6));
assertEquals(12, h.apply(2));
}
@Test
void testMultScalar() {
UnaryFunction f = x -> x * x;
var scalar = 2.5;
var h = f.mult(scalar); // h(x) = 2.5 x^2
assertEquals(250, h.apply(10));
assertEquals(2.5, h.apply(1));
assertEquals(2.5, h.apply(-1));
assertEquals(10, h.apply(2));
}
@Test
void testDivZeroDenominator() {
UnaryFunction f = x -> x;
UnaryFunction g = x -> 0;
var h = f.div(g);
assertEquals(0, h.apply(0));
assertEquals(Double.POSITIVE_INFINITY, h.apply(1));
assertEquals(Double.NEGATIVE_INFINITY, h.apply(-1));
}
@Test
void testDivNonzeroDenominator() {
UnaryFunction f = x -> 2 * x;
UnaryFunction g = x -> x + 1;
var h = f.div(g);
assertEquals(0, h.apply(0));
assertEquals(4, h.apply(-2));
assertEquals(16.0 / 9, h.apply(8));
}
@Test
void testDivScalar() {
UnaryFunction f = x -> x * x;
var scalar = 2.5;
var h = f.div(scalar); // h(x) = x^2 / 2.5
assertEquals(10, h.apply(5));
}
@Test
void testExp() {
UnaryFunction f = x -> x * x;
UnaryFunction g = x -> x + 2;
var h = f.exp(g); // h(x) = f(x) ^ g(x) = (x^2)^(x + 2)
assertEquals(1, h.apply(1));
assertEquals(0, h.apply(0));
assertEquals(1, h.apply(-2));
assertEquals(256, h.apply(2)); // (2^2)^(2 + 2) = 4^4
assertEquals(59049, h.apply(3)); // (3^2)^(3 + 2) = 9^5
}
@Test
void testExpScalar() {
UnaryFunction f = x -> x * x;
var scalar = 4;
var h = f.exp(scalar); // h(x) = f(x)^4 = x^8
assertEquals(0, h.apply(0));
assertEquals(1, h.apply(1));
assertEquals(1, h.apply(-1));
assertEquals(256, h.apply(2));
assertEquals(6561, h.apply(3));
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class UnitTest { // :)
@Test
void testOf() {
ExampleUnit u = new ExampleUnit(1);
Measure<ExampleUnit> fiveOfSomething = u.of(5);
assertEquals(5, fiveOfSomething.magnitude(), 0);
assertEquals(u, fiveOfSomething.unit());
}
}

View File

@@ -0,0 +1,348 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static edu.wpi.first.units.Units.Amps;
import static edu.wpi.first.units.Units.Celsius;
import static edu.wpi.first.units.Units.Centimeters;
import static edu.wpi.first.units.Units.Degrees;
import static edu.wpi.first.units.Units.Fahrenheit;
import static edu.wpi.first.units.Units.Feet;
import static edu.wpi.first.units.Units.FeetPerSecond;
import static edu.wpi.first.units.Units.Grams;
import static edu.wpi.first.units.Units.Gs;
import static edu.wpi.first.units.Units.Horsepower;
import static edu.wpi.first.units.Units.Inches;
import static edu.wpi.first.units.Units.Kelvin;
import static edu.wpi.first.units.Units.Kilo;
import static edu.wpi.first.units.Units.Kilograms;
import static edu.wpi.first.units.Units.Meters;
import static edu.wpi.first.units.Units.MetersPerSecond;
import static edu.wpi.first.units.Units.MetersPerSecondPerSecond;
import static edu.wpi.first.units.Units.Microseconds;
import static edu.wpi.first.units.Units.Milli;
import static edu.wpi.first.units.Units.Milliamps;
import static edu.wpi.first.units.Units.Millimeters;
import static edu.wpi.first.units.Units.Milliseconds;
import static edu.wpi.first.units.Units.Millivolts;
import static edu.wpi.first.units.Units.Milliwatts;
import static edu.wpi.first.units.Units.Minutes;
import static edu.wpi.first.units.Units.Ounces;
import static edu.wpi.first.units.Units.Percent;
import static edu.wpi.first.units.Units.Pounds;
import static edu.wpi.first.units.Units.Radians;
import static edu.wpi.first.units.Units.Revolutions;
import static edu.wpi.first.units.Units.Second;
import static edu.wpi.first.units.Units.Seconds;
import static edu.wpi.first.units.Units.Value;
import static edu.wpi.first.units.Units.Volts;
import static edu.wpi.first.units.Units.Watts;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class UnitsTest {
// Be accurate to 0.01%
private static final double thresh = 1e-5;
void testBaseUnit(Unit<?> baseUnit) {
assertEquals(0, baseUnit.of(0).baseUnitMagnitude(), 0);
assertEquals(1, baseUnit.of(1).baseUnitMagnitude(), 0);
assertEquals(-1, baseUnit.of(-1).baseUnitMagnitude(), 0);
assertEquals(100, baseUnit.of(100).baseUnitMagnitude(), 0);
assertEquals(8.281723, baseUnit.of(8.281723).baseUnitMagnitude(), 0);
assertEquals(Float.MAX_VALUE, baseUnit.of(Float.MAX_VALUE).baseUnitMagnitude(), 0);
assertEquals(Float.MIN_VALUE, baseUnit.of(Float.MIN_VALUE).baseUnitMagnitude(), 0);
}
// Distances
@Test
void testMeters() {
testBaseUnit(Meters);
assertEquals("Meter", Meters.name());
assertEquals("m", Meters.symbol());
}
@Test
void testMillimeters() {
assertEquals(1000, Millimeters.convertFrom(1, Meters), thresh);
assertEquals(1, Meters.convertFrom(1000, Millimeters), thresh);
assertEquals("Millimeter", Millimeters.name());
assertEquals("mm", Millimeters.symbol());
}
@Test
void testCentimeters() {
assertEquals(100, Centimeters.convertFrom(1, Meters), thresh);
assertEquals(1, Meters.convertFrom(100, Centimeters), thresh);
assertEquals("Centimeter", Centimeters.name());
assertEquals("cm", Centimeters.symbol());
}
@Test
void testInches() {
assertEquals(1, Meters.convertFrom(39.3701, Inches), thresh);
assertEquals(39.3701, Inches.convertFrom(1, Meters), 1e-4);
assertEquals(1 / 25.4, Inches.convertFrom(1, Millimeters), 0); // should be exact
assertEquals(12, Inches.convertFrom(1, Feet), thresh);
assertEquals("Inch", Inches.name());
assertEquals("in", Inches.symbol());
}
@Test
void testFeet() {
assertEquals(3.28084, Feet.convertFrom(1, Meters), thresh);
assertEquals(1 / 12.0, Feet.convertFrom(1, Inches), thresh);
assertEquals("Foot", Feet.name());
assertEquals("ft", Feet.symbol());
}
// Velocities
@Test
void testMetersPerSecond() {
testBaseUnit(MetersPerSecond);
assertEquals("Meter per Second", MetersPerSecond.name());
assertEquals("m/s", MetersPerSecond.symbol());
}
@Test
void testFeetPerSecond() {
assertEquals(3.28084, FeetPerSecond.convertFrom(1, MetersPerSecond), thresh);
assertEquals(1 / 3.28084, MetersPerSecond.convertFrom(1, FeetPerSecond), thresh);
assertEquals("Foot per Second", FeetPerSecond.name());
assertEquals("ft/s", FeetPerSecond.symbol());
}
// Accelerations
@Test
void testMetersPerSecondPerSecond() {
testBaseUnit(MetersPerSecondPerSecond);
assertEquals("Meter per Second per Second", MetersPerSecondPerSecond.name());
assertEquals("m/s/s", MetersPerSecondPerSecond.symbol());
assertEquals(MetersPerSecond, MetersPerSecondPerSecond.getUnit());
assertEquals(Seconds, MetersPerSecondPerSecond.getPeriod());
}
@Test
void testGs() {
assertEquals(32.17405, Gs.of(1).in(FeetPerSecond.per(Second)), thresh);
assertEquals(9.80665, Gs.of(1).in(MetersPerSecondPerSecond), thresh);
assertEquals("G", Gs.name());
assertEquals("G", Gs.symbol());
assertEquals(Units.AnonymousBaseUnit, Gs.getUnit());
assertEquals(Seconds, Gs.getPeriod());
}
// Time
@Test
void testSeconds() {
testBaseUnit(Seconds);
assertEquals("Second", Seconds.name());
assertEquals("s", Seconds.symbol());
}
@Test
void testMillisecond() {
assertEquals(1000, Milliseconds.convertFrom(1, Seconds), thresh);
assertEquals("Millisecond", Milliseconds.name());
assertEquals("ms", Milliseconds.symbol());
}
@Test
void testMicrosecond() {
assertEquals(1e6, Microseconds.convertFrom(1, Second), 0);
assertEquals("Microsecond", Microseconds.name());
assertEquals("us", Microseconds.symbol());
}
@Test
void testMinutes() {
assertEquals(60, Seconds.convertFrom(1, Minutes), thresh);
assertEquals("Minute", Minutes.name());
assertEquals("min", Minutes.symbol());
}
// Mass
@Test
void testKilograms() {
testBaseUnit(Kilograms);
assertEquals("Kilogram", Kilograms.name());
assertEquals("Kg", Kilograms.symbol());
}
@Test
void testGrams() {
assertEquals(1000, Grams.convertFrom(1, Kilograms), thresh);
assertEquals("Gram", Grams.name());
assertEquals("g", Grams.symbol());
}
@Test
void testPounds() {
assertEquals(453.592, Grams.convertFrom(1, Pounds), thresh);
assertEquals("Pound", Pounds.name());
assertEquals("lb.", Pounds.symbol());
}
@Test
void testOunces() {
assertEquals(16, Ounces.convertFrom(1, Pounds), thresh);
assertEquals("Ounce", Ounces.name());
assertEquals("oz.", Ounces.symbol());
}
// Angle
@Test
void testRevolutions() {
testBaseUnit(Revolutions);
assertEquals("Revolution", Revolutions.name());
assertEquals("R", Revolutions.symbol());
}
@Test
void testRadians() {
assertEquals(2 * Math.PI, Radians.convertFrom(1, Revolutions), thresh);
assertEquals(2 * Math.PI, Radians.convertFrom(360, Degrees), thresh);
assertEquals("Radian", Radians.name());
assertEquals("rad", Radians.symbol());
}
@Test
void testDegrees() {
assertEquals(360, Degrees.convertFrom(1, Revolutions), thresh);
assertEquals(360, Degrees.convertFrom(2 * Math.PI, Radians), thresh);
assertEquals("Degree", Degrees.name());
assertEquals("°", Degrees.symbol());
}
// Unitless
@Test
void testValue() {
testBaseUnit(Value);
assertEquals("<?>", Value.name());
assertEquals("<?>", Value.symbol());
}
@Test
void testPercent() {
assertEquals(100, Percent.convertFrom(1, Value), thresh);
assertEquals("Percent", Percent.name());
assertEquals("%", Percent.symbol());
}
// Electric potential
@Test
void testVolts() {
testBaseUnit(Volts);
assertEquals("Volt", Volts.name());
assertEquals("V", Volts.symbol());
}
@Test
void testMillivolts() {
assertEquals(1000, Millivolts.convertFrom(1, Volts), thresh);
assertEquals("Millivolt", Millivolts.name());
assertEquals("mV", Millivolts.symbol());
}
// Electric current
@Test
void testAmps() {
testBaseUnit(Amps);
assertEquals("Amp", Amps.name());
assertEquals("A", Amps.symbol());
}
@Test
void testMilliamps() {
assertEquals(1000, Milliamps.convertFrom(1, Amps), thresh);
assertEquals("Milliamp", Milliamps.name());
assertEquals("mA", Milliamps.symbol());
}
// Power
@Test
void testWatts() {
testBaseUnit(Watts);
assertEquals("Watt", Watts.name());
assertEquals("W", Watts.symbol());
}
@Test
void testMilliwatts() {
assertEquals(1000, Milliwatts.convertFrom(1, Watts), thresh);
assertEquals("Milliwatt", Milliwatts.name());
assertEquals("mW", Milliwatts.symbol());
}
@Test
void testHorsepower() {
assertEquals(745.7, Watts.convertFrom(1, Horsepower), thresh);
assertEquals("Horsepower", Horsepower.name());
assertEquals("HP", Horsepower.symbol());
}
// Temperature
@Test
void testKelvin() {
testBaseUnit(Kelvin);
assertEquals("Kelvin", Kelvin.name());
assertEquals("K", Kelvin.symbol()); // note: there's no degree symbol for Kelvin!
}
@Test
void testCelsius() {
assertEquals(0, Celsius.of(-273.15).in(Kelvin), thresh);
assertEquals(273.15, Celsius.of(0).in(Kelvin), thresh);
assertEquals(0, Kelvin.of(273.15).in(Celsius), thresh);
assertTrue(Celsius.of(0).isEquivalent(Kelvin.of(273.15)));
assertTrue(Celsius.of(-273.15).isEquivalent(Kelvin.of(0)));
assertEquals("Celsius", Celsius.name());
assertEquals("°C", Celsius.symbol());
}
@Test
void testFahrenheit() {
assertEquals(0, Fahrenheit.of(32).in(Celsius), thresh);
assertEquals(100, Fahrenheit.of(212).in(Celsius), thresh);
assertEquals(-459.67, Kelvin.of(0).in(Fahrenheit), thresh);
assertEquals(273.15, Fahrenheit.of(32).in(Kelvin), thresh);
assertEquals(32, Kelvin.of(273.15).in(Fahrenheit), thresh);
assertTrue(Fahrenheit.of(32).isEquivalent(Celsius.of(0)));
assertTrue(Fahrenheit.of(212).isEquivalent(Celsius.of(100)));
assertEquals("Fahrenheit", Fahrenheit.name());
assertEquals("°F", Fahrenheit.symbol());
}
// Helpers
@Test
void testKilo() {
ExampleUnit unit = new ExampleUnit(1);
ExampleUnit kiloUnit = Kilo(unit);
assertEquals(1e3, unit.convertFrom(1, kiloUnit), 0);
assertEquals(1e-3, kiloUnit.convertFrom(1, unit), 0);
}
@Test
void testMilli() {
ExampleUnit unit = new ExampleUnit(1);
ExampleUnit milliUnit = Milli(unit);
assertEquals(1e-3, unit.convertFrom(1, milliUnit), 0);
assertEquals(1e3, milliUnit.convertFrom(1, unit), 0);
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static edu.wpi.first.units.Units.Feet;
import static edu.wpi.first.units.Units.FeetPerSecond;
import static edu.wpi.first.units.Units.Meters;
import static edu.wpi.first.units.Units.MetersPerSecond;
import static edu.wpi.first.units.Units.MetersPerSecondPerSecond;
import static edu.wpi.first.units.Units.Millisecond;
import static edu.wpi.first.units.Units.Second;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class VelocityTest {
@Test
void testBaseUnit() {
assertTrue(MetersPerSecond.equivalent(BaseUnits.Velocity));
assertTrue(Meters.per(Second).equivalent(BaseUnits.Velocity));
}
@Test
void testToAcceleration() {
Velocity<Velocity<Distance>> metersPerSecondPerMillisecond = MetersPerSecond.per(Millisecond);
assertEquals(1000, metersPerSecondPerMillisecond.of(1).in(MetersPerSecondPerSecond), 0);
assertEquals(0, metersPerSecondPerMillisecond.of(0).in(MetersPerSecondPerSecond), 0);
}
@Test
void testCache() {
assertSame(
FeetPerSecond, Feet.per(Second), "Feet.per(Second) should return a cached object instance");
// completely arbitrary units chosen because they won't have already been cached
var someDistance = new ExampleUnit(5);
var someTime = new ExampleUnit(600);
var firstInvocation = someDistance.per(someTime);
var secondInvocation = someDistance.per(someTime);
assertSame(
firstInvocation,
secondInvocation,
firstInvocation + " was not the same object as " + secondInvocation);
}
@Test
void testMult() {
var baseUnit = new ExampleUnit(92);
var vel = baseUnit.per(Millisecond);
var mult = vel.mult(Second);
assertEquals(92_000, mult.toBaseUnits(1), 1e-5);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class VoltageTest {
@Test
void testVoltsTimesAmps() {
assertTrue(Units.Volts.times(Units.Amps, "", "").equivalent(Units.Watts));
}
@Test
void testMilliVoltsTimesMilliAmps() {
// results in microwatts
assertTrue(
Units.Millivolts.times(Units.Milliamps, "", "").equivalent(Units.Milli(Units.Milliwatts)));
}
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.units.collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class LongToObjectHashMapTest {
@Test
void put() {
var map = new LongToObjectHashMap<String>();
assertEquals(0, map.size());
long key = 42;
var previousValue = map.put(key, "The Meaning of Life");
assertNull(previousValue);
assertEquals(1, map.size());
assertEquals("The Meaning of Life", map.get(key));
assertTrue(map.containsKey(key));
previousValue = map.put(key, "Addendum: Live Long and Prosper");
assertEquals("The Meaning of Life", previousValue);
assertEquals(1, map.size(), "The map should not have grown when changing the value for a key");
assertTrue(map.containsKey(key));
var removed = map.remove(key);
assertEquals("Addendum: Live Long and Prosper", removed);
assertEquals(0, map.size());
assertFalse(map.containsKey(key));
}
@Test
void testGrowth() {
var map = new LongToObjectHashMap<String>();
int numInserts = 8000;
for (int i = 0; i < numInserts; i++) {
map.put(i, "value-" + i);
}
assertEquals(numInserts, map.size());
assertEquals(16384, map.capacity());
for (int i = 0; i < numInserts; i++) {
assertEquals("value-" + i, map.get(i), "key " + i + " had an unexpected value");
assertTrue(map.containsKey(i));
}
for (int i = 0; i < numInserts; i++) {
map.remove(i);
}
// all items should have been removed
assertEquals(0, map.size());
// ... but the map shouldn't resize
assertEquals(16384, map.capacity());
}
@Test
void testClear() {
var map = new LongToObjectHashMap<>();
for (int i = 0; i < 10_000; i++) {
map.put(i, i * i);
}
assertEquals(10_000, map.size());
assertEquals(16384, map.capacity());
map.clear();
assertEquals(0, map.size());
assertEquals(16384, map.capacity());
map.clear(); // should be a NOP
assertEquals(0, map.size());
assertEquals(16384, map.capacity());
}
@Test
void testKeySet() {
var map = new LongToObjectHashMap<String>();
int numInserts = 8000;
for (int i = 0; i < numInserts; i++) {
map.put(i, "value-" + i);
}
var keySet = map.keySet();
assertEquals(map.size(), keySet.size());
for (int i = 0; i < numInserts; i++) {
assertTrue(keySet.contains(i), "keySet does not contain key " + i);
}
}
}