Measures

The Measure class provides the core of the functionality for Safe Units. A Measure is a value associated with a unit (e.g. 5 meters, 10 seconds) in a given unit system (e.g. SI units). A Measure may be given a symbol to indicate that it itself represents some unit. For example, 0.3048 meters is one foot and therefore might be given the symbol ft. Measures are immutable and any operation on a measure returns a new measure.

The Measure class takes two type parameters B and U. The B type parameter is the basis for the unit system being used by the measure. The U type parameter is the type of the unit that the measure represents.

Note: The Measure class provides functionality for manipulating units with numeric values represented by the JavaScript number type. For different numeric types see Generic Measures.

Construction

The following functions are static functions on the Measure class used to construct or manipulate measures.

Measure.of

Measure.of<B, U>(
   value: number,
   quantity: Measure<B, U>,
   symbol?: string,
): Measure<B, U>

Creates a measure that is a scalar multiple of a given measure. An optional symbol may be provided to name the resulting measure.

Examples:

const d1 = Measure.of(30, meters);
const feet = Measure.of(0.3048, meters, "ft");
const d2 = Measure.of(10, feet);
const minutes = Measure.of(60, seconds, "min");

Measure.dimensionless

Measure.dimensionless<B>(
   unitSystem: UnitSystem<B>,
   value: number,
): Measure<B, DimensionlessUnit<B>>

Creates a dimensionless measure value in the given unit system.

Examples:

const scalar = Measure.dimensionless(SIUnitSystem, 2);
const distance = Measure.of(20, meters);
const doubled = distance.times(scalar); // 40 m

Measure.dimension

Measure.dimension<B, D extends keyof B>(
   unitSystem: UnitSystem<B>,
   dimension: D,
   symbol?: string,
): Measure<...>

Constructs a measure which represents a single dimension of the given unit system. The resulting measure is 1 of the base unit for that dimension. For more information see Defining Quantities.

const meter = Measure.dimension(SIUnitSystem, "length");
console.log(meter); // 1 m

const second = Measure.dimension(SIUnitSystem, "time");
console.log(second); // 1 s

Measure.isMeasure

Measure.isMeasure(value: any): value is Measure<any, any>

Since Measure isn't technically a class, you can't check that a value is a measure by using instanceof. Instead, use this method to determine if a given value is a Measure.

Measure.prefix

Measure.prefix(prefix: string, multiplier: number): PrefixFn

Creates a function which, when given a measure, applies a prefix to that measure's symbol and multiplies it by a given dimensionless value.

Examples:

const kilo = Measure.prefix("k", 1000);
const kilometers = kilo(meters); // 1000 m

const distance = Measure.of(20, kilometers); // 20000 m
distance.in(kilometers); // 20 km

Operations

Negation

Measure<B, U>.negate(): Measure<B, U>

Returns a new measure containing the negative value of the original.

Examples:

const positive = Measure.of(30, meters);
const negative = positive.negate(); // -30 m

Addition

// Instance method
Measure<B, U>.plus(other: Measure<B, U>): Measure<B, U>

// Static functions
Measure.add<B, U>(left: Measure<B, U>, right: Measure<B, U>): Measure<B, U>
Measure.sum<B, U>(first: Measure<B, U>, ...rest: Array<Measure<B, U>>): Measure<B, U>

Measures may only be added if they have the same unit. This will produce a new measure with the same unit. The Measure.add static function is an alias for a.plus(b). The Measure.sum method must be given a list of one or more units, since we can't infer the unit for an empty list.

Examples:

const d1 = Measure.of(30, meters);
const d2 = Measure.of(10, meters);
const d3 = Measure.of(100, feet);
const t1 = Measure.of(2, minutes);

const sum1 = d1.plus(d2); // 40 m
const sum2 = Measure.add(d1, d2); // 40 m
const sum3 = Measure.sum(d1, d2, d3); // 140m

const good = d1.plus(d3); // Fine because both are lengths

// @ts-expect-error Cannot add measures representing distances and times together
const bad = d1.plus(t1);

Subtraction

// Instance method
Measure<B, U>.minus(other: Measure<B, U>): Measure<B, U>

// Static function
Measure.subtract<B, U>(left: Measure<B, U>, right: Measure<B, U>): Measure<U>

Measures may only be subtracted if they have the same unit. This will produce a new measure with the same unit. All of these functions behave the same.

const d1 = Measure.of(30, meters);
const d2 = Measure.of(10, meters);
const t1 = Measure.of(2, minutes);

const diff1 = d1.minus(d2); // -20 m
const diff2 = Measure.subtract(d2, d1); // 20 m

// @ts-expect-error Cannot subtract a distance from a time unit
const bad = t1.minus(d1);

Multiplication

// Instance method
Measure<B, U>.times<V>(other: Measure<B, V>): Measure<B, MultiplyUnits<B, U, V>>

// Static function
Measure.multiply<B, U, V>(
   left: Measure<B, U>,
   right: Measure<B, V>,
): Measure<B, MultiplyUnits<B, U, V>>

Multiplies two measures together and returns a new measure. The resulting unit is computed at compile time to be the result of multiplying the arguments' units together.

const mass = Measure.of(10, kilograms);
const acceleration = Measure.of(9.8, metersPerSecondsSquared);
const time = Measure.of(10, seconds);

// Works! The result of mass times acceleration is force
const force: Force = mass.times(acceleration); // 98 N
const velocity: Velocity = Measure.multiply(acceleration, time); // 98 m/s

// @ts-expect-error A force quantity cannot be assigned to a pressure quantity
const bad: Pressure = Measure.multiply(mass, acceleration);

Division

// Instance methods
Measure<B, U>.div<V>(other: Measure<B, V>): Measure<B, DivideUnits<B, U, V>>
Measure<B, U>.over<V>(other: Measure<B, V>): Measure<B, DivideUnits<B, U, V>>
Measure<B, U>.per<V>(other: Measure<B, V>): Measure<B, DivideUnits<B, U, V>>

// Static function
Measure.divide<B, U, V>(
   left: Measure<B, U>,
   right: Measure<B, V>,
): Measure<B, DivideUnits<B, U, V>>

Divides two measures together and returns a new measure. The resulting unit is computed at compile time to be the result of dividing the arguments' units together. All of these functions behave the same and are provided to make writing readable units easier.

const distance = Measure.of(30, meters);
const time = Measure.of(10, seconds);

// Works! The result of distance over time is velocity
const velocity: Velocity = distance.over(time); // 300 m*s
const acceleration: Acceleration = velocity.div(time); // 30 m/s^2

// @ts-expect-error A velocity quantity cannot be assigned to an acceleration quantity
const bad: Acceleration = Measure.divide(distance, time);

Scalar Multiplication

Measure<B, U>.scale(value: number): Measure<B, U>

A convenience method for multiplying a measure by a dimensionless value.

const t = Measure.of(10, seconds);
const doubledShort = t.scale(2); // 20 s
const doubledLong = t.times(Measure.dimensionless(SIUnitSystem, 2)); // 20 s

Exponentiation

Measure<B, U>.squared(): Measure<B, SqareUnit<B, U>>
Measure<B, U>.cubed(): Measure<B, CubeUnit<B, U>>

Convenience methods to square and cube a measure respectively. These are equivalent to multiplying a unit by itself the corresponding number of times.

Examples:

const side = Measure.of(10, meters);

const area: Area = side.squared(); // 100 m^2
const volume: Volume = side.cubed(); // 1000 m^3

Reciprocals

Measure<B, U>.reciprocal(): Measure<B, ReciprocalUnit<B, U>>
Measure<B, U>.inverse(): Measure<B, ReciprocalUnit<B, U>>

Computes the reciprocal of the value and unit of the measure. Both methods are identical.

Examples:

const freq: Frequency = Measure.of(30, hertz); // 30 1/s

const cycle: Time = freq.inverse(); // 1/30 s

Static Math

The Measure class contains a number of static methods from the JavaScript Math object wrapped to work on measures. Many of these functions take in a single measure and return a single measure without changing its unit:

  • Measure.abs
  • Measure.ceil
  • Measure.floor
  • Measure.fround
  • Measure.round
  • Measure.trunc

There is also a wrapper for Math.hypot that takes in one or more measures all with the same unit and returns a single measure of that unit.

Examples:

const distance = Measure.of(-9.8, meters);

Measure.abs(distance); // 9.8 m
Measure.trunc(distance); // -9 m

const width = Measure.of(3, meters);
const height = Measure.of(4, meters);

Measure.hypot(width, height); // 5 m

Comparisons

Measure<B, U>.lt(other: Measure<B, U>): boolean;
Measure<B, U>.lte(other: Measure<B, U>): boolean;
Measure<B, U>.eq(other: Measure<B, U>): boolean;
Measure<B, U>.neq(other: Measure<B, U>): boolean;
Measure<B, U>.gte(other: Measure<B, U>): boolean;
Measure<B, U>.gt(other: Measure<B, U>): boolean;

Measures are only comparable if they are of the same unit. Attempting to compare measures with different units is a compile time error.

Examples:

const t1 = Measure.of(30, minutes);
const t2 = Measure.of(0.25, hours);
const d1 = Measure.of(10, meters);

t1.gt(t2); // true

// @ts-expect-error Cannot compare time and distance values
t1.eq(d1);

Symbols

Measure<B, U>.withSymbol(symbol: string): Measure<B, U>;

Duplicates the current measure and gives the new measure a symbol. Symbols are specific to an instance of a measure, performing operations on that measure will not forward along any symbols to the resulting measures. Calling measure.withSymbol(symbol) is equivalent to calling Measure.of(1, measure, symbol). The symbol of a measure can be seen by accessing the readonly symbol field.

const squareMeters = meters.squared().withSymbol("sq. m");

squareMeters.symbol; // "sq. m"

// All of the following lose the symbol from squareMeters:
const r1 = squareMeters.scale(2);
const r2 = Measure.of(10, squareMeters);
const r3 = Measure.divide(squareMeters, meters);

Symbols are used in converting other measures into strings as can be seen below.

Formatting

Measure<B, U>.toString(formatter?: MeasureFormatter): string;

Returns a string version of the unit, ignoring any symbol information on that measure. This method optionally takes a formatter which has the following interface:

interface MeasureFormatter {
    formatValue?: (value: number) => string;
    formatUnit?: (unit: Unit, unitSystem: UnitSystem) => string;
}

The formatValue function, if provided, will be applied to customize the formatting of the numeric value of the measure in the resulting string. The formatUnit, if provided, will be passed the unit and unit system of the measure in order to customize how that is formatted. When calling the Measure.in method, the formatUnit function will only be used if the unit being used to express the measure has no symbol.

Examples:

const kilometers = Measure.of(1000, meters, "km");
// Could also be written as: Measure.of(1000, meters).withSymbol("km")

const distance = Measure.of(5, kilometers);
distance.toString(); // "5000 m"
distance.toString({
    formatValue: x => x.toExponential(),
    formatUnit: () => "meters",
}); // "5e+3 meters"

const force = Measure.of(30, newtons);
force.toString(); // "30 kg * m * s^-2"
force.toString({ formatValue: x => x.toPrecision(5) }); // "30.000 kg * m * s^-2"

Conversions

Measure<B, U>.in(unit: Measure<B, U>, formatter?: MeasureFormatter): string;
Measure<B, U>.valueIn(unit: Measure<B, U>): number;

These methods can be used if you want to display or represent a measure in terms of another measure that represents a unit. The measures must have the same unit in order for the conversion to be valid.

The in method will return a formatted string containing the unit information. This takes an optional formatter parameter that behaves just like the toString() method.

The valueIn will just return the numeric value of the conversion. Be careful when using valueIn as this erases the type information of the unit and is no longer type safe.

Examples:

const kilometers = Measure.of(1000, meters, "km");
const distance = Measure.of(5500, meters);

distance.in(kilometers); // "5.5 km"
distance.in(kilometers, { formatValue: x => x.toPrecision(3) }); // "5.50 km"
distance.valueIn(kilometers); // 5.5

Unsafe Mappings

Measure<B, U>.unsafeMap(valueMap: (value: number) => number): Measure<B, U>;
Measure<B, U>.unsafeMap<V>(
   valueMap: (value: number) => number,
   unitMap: (unit: U) => V,
): Measure<B, V>;

If only one argument is passed, performs a mapping on the value of a measure without affecting the unit of the measure. If both arguments are passed maps both the unit and value of a measure. This is generally used for internal purposes and should be avoided whenever possible. Instead consider using a function wrapper.

Representation

A Measure represents a value in terms its base units. For example, in the SI unit system, the base unit for length, mass, and time are meters, kilograms, and seconds respectively. We can define values units for feet, pounds, and minutes in this system but they will be represented under the hood as those base units.

const feet: Length = Measure.of(0.3048, meters, "ft");
const pounds: Mass = Measure.of(0.453592, kilograms, "lb");
const minutes: Time = Measure.of(60, seconds, "min");

If we use these units to derive new values, those resulting values will still be represented using meters, kilograms, and seconds:

const yards = Measure.of(3, feet); // 0.9144 m
const stones = Measure.of(14, pounds); // 6.35029 kg
const hours = Measure.of(60, minutes); // 3600 s

In this way, we never actually have to perform unit conversions since, under the hood, all measures with the same dimension are always represented with the same units. We can format these values using any unit we want with the in method

Function Wrappers

It is often desirable to convert operations on numbers into operations on measures. Frequently, these functions make no change on the unit of a value. For example, suppose we want to make an absolute value function that operates on measures. We'd expect the function perserve the unit of the input. We can simply wrap an existing absolute value function using wrapUnaryFn:

const measureAbs = wrapUnaryFn(Math.abs);
const time = Measure.of(-30, seconds);
measureAbs(time); // 30 s

The following function wrappers are provided:

  • wrapUnaryFn - Wraps a function of a single number.
  • wrapBinaryFn - Wraps a function of two numbers, returning a function that expects two measures of the same unit.
  • wrapSpreadFn - Wraps a function that takes any number of numbers and returns a function that takes one or more measures of the same unit.
  • `wrapReducerFn* - Wraps a function that takes two numbers and returns a function which takes one or more measures and performs a reduce across its inputs.
Safe Units is developed by Jonah Scheinerman. Please contact me if you have questions or concerns.
Safe Units is distributed under the MIT open source license.

Copyright © 2024 by Jonah Scheinerman