/unitized

A nice API for handling numbers with associated units

Primary LanguageTypeScriptMIT LicenseMIT

@speleotica/unitized

CircleCI Coverage Status semantic-release Commitizen friendly npm version

A nice API for handling numbers with associated units.

Only units relevant to cave surveying are built into this package, but it's possible to define your own using the API.

Motivation

I've dealt with a lot of unit conversion bugs over the years. There are usually two root causes:

  • storing the associated unit in a separate variable than a number
  • forgetting to perform a unit conversion somewhere during a calculation

To cut down on these kinds of mistakes, now I try to always store the number and its unit together in a single object, and perform calculations on those objects instead of directly on the numbers. I only deal with raw numbers at the input or output boundaries of an API.

The TypeScript types are also designed to help you avoid accidentally mixing quantities of different unit types, for instance lengths and angles.

Here are some examples of what the API for this looks like:

Unitize.feet(1).add(Unitize.inches(6)) // 1.5 ft
Unitize.feet(1).get(Length.meters) // 0.3048 m
Unitize.feet(2).div(Unitize.inches(4)) // 6

type Point = {
  northing: UnitizedNumber<Length>,
  easting: UnitizedNumber<Length>,
  elevation: UnitizedNumber<Length>,
}

type SurveyLeg = {
  distance: UnitizedNumber<Length>,
  azimuth: UnitizedNumber<Angle>,
  inclination: UnitizedNumber<Angle>,
}

function calculateLeg(
  from: Point,
  { distance, azimuth, inclination }: SurveyLeg
) {
  const xy = distance.mul(Angle.cos(inclination))
  const northing = xy.mul(Angle.cos(azimuth))
  const easting = xy.mul(Angle.sin(azimuth))
  const elevation = distance.mul(Angle.sin(inclination))

  return {
    northing: from.northing.add(northing),
    easting: from.easting.add(easting),
    elevation: from.elevation.add(elevation),
  }
}

API

Unitize

import { Unitize } from '@speleotica/unitized'

Contains shortcut functions for making unitized numbers. For example, Unitize.meters(2) is equivalent to new UnitizedNumber(2, Length.meters).

  • Unitize.meters
  • Unitize.centimeters
  • Unitize.kilometers
  • Unitize.feet
  • Unitize.inches
  • Unitize.yards
  • Unitize.miles
  • Unitize.radians
  • Unitize.degrees
  • Unitize.gradians (1/400 of a unit circle)
  • Unitize.milsNATO (1/6400 of a unit circle)
  • Unitize.percentGrade (rise over run as %; 100% = 45 degrees)

Length

import { Length } from '@speleotica/unitized'

Contains length units:

  • Length.meters
  • Length.centimeters
  • Length.kilometers
  • Length.feet
  • Length.inches
  • Length.yards
  • Length.miles

Each of these units is an instance of Unit<Length>.

Angle

import { Angle } from '@speleotica/unitized'

Contains angle units:

  • Angle.radians
  • Angle.degrees
  • Angle.gradians (1/400 of a unit circle)
  • Angle.milsNATO (1/6400 of a unit circle)
  • Angle.percentGrade (rise over run as %; 100% = 45 degrees)

Each of these units is an instance of Unit<Angle>.

static sin(angle: UnitizedNumber<Angle>): number

Computes the sine of the given angle.

static cos(angle: UnitizedNumber<Angle>): number

Computes the cosine of the given angle.

static tan(angle: UnitizedNumber<Angle>): number

Computes the tangent of the given angle.

static asin(sin: number): UnitizedNumber<Angle>

Computes the arcsine of the given number.

static acos(cos: number): UnitizedNumber<Angle>

Computes the arccosine of the given number.

static atan(tan: number): UnitizedNumber<Angle>

Computes the arctangent of the given number.

static atan2(y: number, x: number): nUnitizedNumber<Angle>

Equivalent to Math.atan2, but returns a UnitizedNumber<Angle>.

static atan2(y: UnitizedNumber<Length>, x: UnitizedNumber<Length>): nUnitizedNumber<Angle>

Equivalent to Math.atan2, but returns a UnitizedNumber<Angle>.

static normalize(angle: UnitizedNumber<Angle>): UnitizedNumber<Angle>

Normalizes the given angle to the range [0, one revolution); returns the normalized angle in the same units.

static opposite(angle: UnitizedNumber<Angle>): UnitizedNumber<Angle>

Returns the angle facing the opposite direction, in the same units.

UnitizedNumber<T extends UnitType<T>>

import { UnitizedNumber } from '@speleotica/unitized'

constructor(value: number, unit: Unit<T>)

Creates a UnitizedNumber with the given value and unit

unit: Unit<T>

The unit this UnitizedNumber's value is in.

private value: number

The numeric value of this UnitizedNumber. Accessing this directly is discouraged; use get(unit) instead.

get(unit: Unit<T>): number

Converts this UnitizedNumber's value to the given unit.

add(addend: UnitizedNumber<T>): UnitizedNumber<T>

Returns this + addend as a new UnitizedNumber in the same units as this.

get isFinite(): boolean

Returns true iff the numeric value is not NaN or infinite.

get isInfinite(): boolean

Returns true iff the numeric value is infinite.

get isNaN(): boolean

Returns true iff the numeric value is NaN.

in(unit: Unit<T>): UnitizedNumber<T>

Returns a new UnitizedNumber in the given unit.

negate(): UnitizedNumber<T>

Returns a new UnitizedNumber with the same units and negative value.

sub(subtrahend: UnitizedNumber<T>): UnitizedNumber<T>

Returns this - subtrahend as a new UnitizedNumber in the same units as this.

mul(multiplicand: number): UnitizedNumber<T>

Returns this * multiplicand as a new UnitizedNumber in the same units as this.

get isNegative(): boolean

Returns true iff the numeric value is negative.

get isPositive(): boolean

Returns true iff the numeric value is positive.

get isZero(): boolean

Returns true iff the numeric value is 0.

get isNonzero(): boolean

Returns true iff the numeric value is not 0.

mod(modulus: UnitizedNumber<T>): UnitizedNumber<T>

Returns this % modulus as a UnitizedNumber in the same units as this.

abs(): UnitizedNumber<T>

Returns a new UnitizedNumber with the same units and absolute value.

div(denominator: UnitizedNumber<T>): number

Returns this / denominator.

div(denominator: number): UnitizedNumber<T>

Returns this / denominator as a UnitizedNumber in the same units as this.

compare(other: UnitizedNumber<T>): number

Returns > 0 if this > other, < 0 if this < other, and 0 otherwise.

class UnitType<T extends UnitType<T>>

import { UnitType } from '@speleotica/unitized'

A type of unit, for example length or angle or temperature.

convert(value: number, from: Unit<T>, to: Unit<T>): number

Converts a value from one unit to another. The default implementation just returns

to.fromBase(from.toBase(value))

If you want more precision you can use a FactorTableUnitType to provide a table of conversion factors from one unit to another, or override this method in a derived class.

class Unit<T extends UnitType<T>>

import { Unit } from '@speleotica/unitized'

constructor(type: UnitType<T>, id: string, props)

Props may have fromBaseFactor and toBaseFactor, which will be used for the default fromBase and toBase implementations (which you may override in a derived class).

type: UnitType<T>

The type of this unit.

id: string

The unique id of this unit.

fromBase(value: number): number

Converts the given number from this unit to the base unit. Only UnitType should call this method. You may override this method in a derived class for nonlinear units.

toBase(value: number): number

Converts the given number from the base unit to this unit. Only UnitType should call this method. You may override this method in a derived class for nonlinear units.

class FactorTableUnitType<T extends FactorTableUnitType<T>> extends UnitType<T>

import { FactorTableUnitType } from '@speleotica/unitized'

A UnitType that uses a table of factors to more accurately convert from one unit to another (instead of converting to some base unit as an intermediary).

constructor(factors: Record<string, Record<string, number>>)

Factors is the conversion table, with unit ids as keys. value * factors[from.id][to.id] is used to convert from one unit to another. Not all pairs of units have to be included in the table; convert will fall back to converting to the base unit as an intermediary if a factor isn't found in this table.