ericman314 / UnitMath

JavaScript library for unit conversion and arithmetic
Apache License 2.0
29 stars 7 forks source link

UnitMath

UnitMath is a JavaScript library for unit conversion and arithmetic.

Build Status codecov

Install

npm install unitmath

Getting Started

const unit = require('unitmath')

let a = unit('5 m').div('2 s')   // 2.5 m / s
let b = unit('40 km').to('mile')  // 24.8548476894934 mile
b.toString({ precision: 4 })          // "24.85 mile"

Creating Units

To create a unit, call unit with either a single string, or a number and a string:

// String
let a = unit('40 mile')
let b = unit('hour')

// Number and string
let g = unit(9.8, 'm/s^2')
let h = unit(19.6, 'm')

Units can be simple (4 kg) or compound (8.314 J/mol K). They may also be valueless (hour).

Parsing Rules

Parsing rules are different than typical JavaScript syntax. For instance:

The following all form valid units:

// These are valid:
unit("2 in")
unit("60/s")
unit("8.314 * kg * m^2 / (s^2 * mol * K)") 
unit("6.022e-23 mol^-1")
unit("kW / kg K")
unit("3.1415926535897932384626433832795 rad") 

// These are invalid:
unit("2 in + 3 in") // Unexpected "+" (Use .add() instead)
unit("60 / s / s") // Unexpected additional "/"
unit("0x123 kg") // Unit "x123" not found

The exact syntax is as follows:

[value][numerator][/denominator]

numerator, denominator:
atomicUnit [atomicUnit ...]

atomicUnit:
[prefix]unit[^power]

value, power:
Any floating-point number

unit, prefix:
Any built-in or user-defined unit or prefix

Unit Conversion

The to method converts from one unit to another. The two units must have the same dimension.

// Convert units
unit('40 km').to('mile') // 24.8548476894934 mile
unit('kg').to('lbm') // 2.20462262184878 lbm
unit(5000, 'kg').to('N s') // Cannot convert 5000 kg to N s: dimensions do not match

The split method will convert one unit into an array of units like so:

// Split a unit into parts
unit('10 km').split([ 'mi', 'ft', 'in' ]) // [ 6 mi, 1128 ft, 4.78740157486361 in ]
unit('51.4934 deg').split([ 'deg', 'arcmin', 'arcsec' ]) // [ 51 deg, 29 arcmin, 36.24 arcsec ]

Arithmetic

Use the methods add, sub, mul, div, pow, sqrt, and others to perform arithmetic on units. Multiple operations can be chained together:

// Chain operations together
let g = unit(9.8, 'm/s^2')
let h = unit(19.6, 'm')
h.mul(2).div(g).sqrt()   // 2 s

Strings and numbers are implicitly converted to units within a chained expression. When chaining operators, they will execute in order from left to right. This may not be the usual, mathematical order of operations:

// Strings are implicitly converted to units
unit('3 ft').add('6 in').mul(2)   // 7 ft

All of the operators are also available on the top-level unit object as static functions:

// Using static functions
unit.mul(unit.add('3 ft', '6 in'), 2)   // 7 ft

Units are immutable, so every operation on a unit creates a new unit.

Simplify

UnitMath has a simplify method that will attempt to simplify a unit:

// Simplify units
unit('10 / s').simplify() // 10 Hz
unit('J / m').simplify() // N

Because units are immutable, simplify always returns a new unit with the simplified units. It does not modify the original unit.

In the simplify method, simplification is performed in two steps:

  1. Attempting to simplify a compound unit into a single unit in the desired unit system
  2. Choosing an appropriate prefix (if options.autoPrefix is true)

Note: To choose the best prefix without automatically simplifying the Unit, use the applyBestPrefix method.

Here are the options available for the simplify method:

Formatting

Use toString to output a unit as a string:

unit('1 lb').to('kg').toString() // "0.45359237 kg"

The toString method accepts a configuration object. The following options are available:

The toString method outputs a unit exactly as it is represented internally. It does not automatically simplify a unit. This means you will usually want to call simplify before calling toString. How and when to simplify a unit is very subjective. UnitMath cannot anticipate all needs, so the user must explicitly call simplify when needed.

// toString outputs units "as-is".
unit('10 / s').toString() // "10 s^-1"
unit('10 / s').simplify().toString() // "10 Hz"

Configuring

UnitMath can be configured using unit.config(options). The function returns a new instance of UnitMath with the specified configuration options.

// Set the default unit system to "us"
const unit = require('unitmath').config({ system: 'us' })

Available options are:

Because unit.config(options) returns a new instance of UnitMath, is is technically possible to perform operations between units created from different instances. The resulting behavior is undefined, however, so it is probably best to avoid doing this.

Important: unit.config(options) returns a new instance of the factory function, so you must assign the return value of unit.config(options) to some variable, otherwise the new options won't take effect:

let unit = require('unitmath')

// Incorrect, has no effect!
unit.config(options)

// Correct
unit = unit.config(options) 

Querying the current configuration

Call unit.getConfig() to return the current configuration.

// Get the current configuration
unit.getConfig() // { system: 'si', ... }

Extending UnitMath

User-Defined Units

To create a user-defined unit, pass an object with a definitions property to unit.config():

  // Create a new configuration adding a user-defined unit
  unit = unit.config({
    definitions: {
      units: {
        lightyear: { value: '9460730472580800 m' }
      }
    }
  })

  unit('1 lightyear').to('mile') // 5878625373183.61 mile

The definitions object contains four properties which allow additional customization of the unit system: units, prefixGroups, systems, and skipBuiltIns.

definitions.units

This object contains the units that are made available by UnitMath. Each key in definitions.units becomes a new unit. The easiest way to define a unit is to provide a string representation in terms of other units:

// Define new units in terms of existing ones
definitions: {
  units: {
    minute: { value: '60 seconds' },
    newton: { value: '1 kg m/s^2' }
  }
}

Here are all the options you can specify:

definitions.prefixGroups

The definitions.prefixGroups object is used to define strings and associated multipliers that are prefixed to units to change their value. For example, the 'k' prefix in km multiplies the value of the m unit by 1000.

For example:

// Define prefix groups
definitions: {
  prefixGroups: {
    NONE: { '': 1 },
    SHORT: {
      m: 0.001,
      '': 1,
      k: 1000
    },
    LONG: {
      milli: 0.001,
      '': 1,
      kilo: 1000
    }
  }
}

definitions.systems

This object assigns one or more units to a number of systems. Each key in definitions.systems becomes a system. For each system, list all the units that should be associated with that system in an array. The units may be single or compound (m or m/s) and may include prefixes.

Example:

// Define unit systems
definitions: {
  systems: {
    si: ['m', 'kg', 's', 'N', 'J', 'm^3', 'm/s'],
    cgs: ['cm', 'g', 's', 'dyn', 'erg', 'cm^3', 'cm/s'],
    us: ['ft', 'lbm', 's', 'lbf', 'btu', 'gal', 'ft/s']
  }
}

When UnitMath formats a unit, it will try to use one of the units from the specified system.

definitions.skipBuiltIns

A boolean value indicating whether to skip the built-in units. If true, only the user-defined units, prefix groups, and systems that are explicitly included in definitions will be created.

// Skip built-in units: only the user-defined units,
// prefix groups, and systems will be created
definitions: {
  skipBuiltIns: true
}

Querying current unit definitions

You can view all the current definitions by calling unit.definitions(). This object contains all the units, prefix groups, and systems that you have configured, including the built-ins (unless definitions.skipBuiltIns is true).

unit.definitions()

Below is an abbreviated sample output from unit.definitions(). It can serve as a starting point to create your own definitions.

// Sample `definitions` config
{ 
  units: {
    '': { quantity: 'UNITLESS', value: 1 },
    meter: {
      quantity: 'LENGTH',
      prefixGroup: 'LONG',
      formatPrefixes: [ 'nano', 'micro', 'milli', 'centi', '', 'kilo' ],
      value: 1,
      aliases: [ 'meters' ]
    },
    m: {
      prefixGroup: 'SHORT',
      formatPrefixes: [ 'n', 'u', 'm', 'c', '', 'k' ],
      value: '1 meter'
    },
    inch: { value: '0.0254 meter', aliases: [ 'inches', 'in' ] },
    foot: { value: '12 inch', aliases: [ 'ft', 'feet' ] },
    yard: { value: '3 foot', aliases: [ 'yd', 'yards' ] },
    mile: { value: '5280 ft', aliases: [ 'mi', 'miles' ] },
    ...
  },
  prefixGroups: {
    NONE: { '': 1 },
    SHORT: {
      '': 1,
      da: 10,
      h: 100,
      k: 1000,
      ...
      d: 0.1,
      c: 0.01,
      m: 0.001,
      ... 
    },
  },
  systems: {
    si: ['m', 'meter', 's', 'A', 'kg', ...],
    cgs: ['cm', 's', 'A', 'g', 'K', ...],
    us: ['ft', 's', 'A', 'lbm', 'degF', ...]
  }
}

Custom Types

You can extend UnitMath to work with custom types. The type option is an object containing several properties, where each property value is a function that replaces the normal +, -, *, /, and other arithmetic operators used internally by UnitMath.

Example using Decimal.js as the custom type:

// Configure UnitMath to use Decimal.js
const Decimal = require('decimal.js')
const unit = unit.config({
  type: {
    clone: (x) => new Decimal(x),
    conv: (x) => new Decimal(x),
    add: (a, b) => a.add(b),
    sub: (a, b) => a.sub(b),
    mul: (a, b) => a.mul(b),
    div: (a, b) => a.div(b),
    pow: (a, b) => a.pow(b),
    eq: (a, b) => a.eq(b),
    lt: (a, b) => a.lt(b),
    le: (a, b) => a.lte(b),
    gt: (a, b) => a.gt(b),
    ge: (a, b) => a.gte(b),
    abs: (a) => a.abs(),
    round: (a) => a.round(),
    trunc: (a) => Decimal.trunc(a)
  }
})

let u = unit2('2.74518864784926316174649567946 m')

Below is a table of functions, their description, and when they are required:

Function Description Required?
clone: (a: T) => T Create a new instance of the custom type. Always
conv: (a: number \| string \| T) => T Convert a number or string into the custom type. Always
add: (a: T, b: T) => T Add two custom types. Always
sub: (a: T, b: T) => T Subtract two custom types. Always
mul: (a: T, b: T) => T Multiply two custom types. Always
div: (a: T, b: T) => T Divide two custom types. Always
pow: (a: T, b: number) => T Raise a custom type to a power. Always
abs: (a: T) => T Return the absolute value of a custom type. For autoPrefix: true
lt: (a: T, b: T) => boolean Compare two custom types for less than. For autoPrefix: true
le: (a: T, b: T) => boolean Compare two custom types for less than or equal. For autoPrefix: true
gt: (a: T, b: T) => boolean Compare two custom types for greater than. For autoPrefix: true
ge: (a: T, b: T) => boolean Compare two custom types for greater than or equal. For autoPrefix: true
eq: (a: T, b: T) => boolean Compare two custom types for equality. For the equals function
round: (a: T) => T Round a custom type to the nearest integer. For the split function
trunc: (a: T) => T Truncate a custom type to the nearest integer. For the split function

The add, sub, mul, div, and pow functions replace +, -, *, /, and Math.pow, respectively. The clone function should return a clone of your custom type (same value, different object).

The conv function must, at a minimum, be capable of converting both strings and numbers into your custom type. If given a custom type, it should return it unchanged, or return a clone. Among other things, the conv function is used by UnitMath to convert the values of the built-in units to your custom type during initialization.

UnitMath will also use the conv function when constructing units from numbers and strings. If your custom type is representable using decimal or scientific notation (such as 6.022e+23), you can include both the value and the units in a single string:

// Supply a single string, and the numeric portion will be parsed using type.conv
unit('3.1415926535897932384626433832795 rad')

If your custom type cannot be represented in decimal or scientific notation, such as is the case with complex numbers and fractions, you will have to pass your custom type and the unit string separately:

unit(Fraction(1, 2), 'kg')

The functions clone, conv, add, sub, mul, div, and pow are always required. Omitting any of these will cause the config method to throw an error. The other functions are conditionally required, and you will receive an error if you attempt something that depends on a function you haven't provided.

API Reference

In the function signatures below, the T type is the custom type you have provided, or number if you have not provided a custom type.

Constructor

Member Functions

Static Functions

Contributing

This is a community-supported project; all contributions are welcome. Please open an issue or submit a pull request.

Acknowledgements

Many thanks to Jos de Jong (@josdejong), the original author of Unit.js, who suggested the idea of splitting the file off from Math.js and into its own library.

Contributors

License

UnitMath is released under the Apache-2.0 license.