Giter VIP home page Giter VIP logo

uom's Introduction

uom

npm version travis build Coverage Status code style: prettier MIT license

Extensible unit of measure conversion with type safety for typescript

Introduction

This package has functions to handle unit of measures. It works particulary well with typescript in which case it can provice some type safety for amounts of different quantity.

Installation

npm install --save uom

The library is compiled to ES5 and no polyfills are required.

NOTE: The base package uom does only contain the base SI units. In order to get more units to convert between you also need to install the uom-units package:

npm install --save uom-units

Features

Conversion

This feature allows you to convert amounts into different units.

import { Amount } from "uom";
import { Units } from "uom-units";

const amount = Amount.create(10, Units.Meter);
const inch = Amount.valueAs(Units.Inch, amount);

Extension (create your own unit)

A measure system has a number of BaseUnits which is used to create all other derived units in the system which are represented as ProductUnit or TransformedUnit. For example in the SI measure system, m and s are BaseUnitss and they can be used to create the m/s ProductUnit.

In the case that a derived unit can be known by a different name, an AlternateUnit can be used. For example in the SI system the derived unit N/m2 is also known as Pascal.

By using the base units you can create any unit.

import { Amount, Unit } from "uom";
import { Units } from "uom-units";

const myInchUnit = Unit.divideNumber(12.0, Units.Foot);
const amount = Amount.create(10, myInchUnit);
const meter = Amount.valueAs(Units.Meter, amount);

Type safety (typescript only)

By declaring your functions with a signature of typed Amount you can make sure the right type of amounts are inputs to the function.

import { Amount } from "uom";
import { Units } from "uom-units";

const length1 = Amount.create(10, Units.Meter);
const length2 = Amount.create(10, Units.Inch);
const volume1 = Amount.create(10, Units.CubicMeter);

const result = calculate(length1, length2); // OK
const result = calculate(volume1, length2); // Compile error

function calculate(Amount<Length> length1, Amount<Length> length2): Amount<Length> {
    return Amount.plus(length1, length2);
}

Formatting

Asosciating formatting directly with an Unit or Quantity is generally not a good idea. Formatting is application specific and should be implemented within application code. For example, an application may have air flows and water flows that both are of VolumeFlow quantity. In this case you may want separate labels and default units for air flow and water flow. Associating formatting directly with VolumeFlow or its units will not solve this. Instead, try tagging each VolumeFlow field within the application with either air_flow, or water_flow and provide different labels and default units per tag.

However if you are just building something smaller and want quick formatting, this package has some utilities for assigning formats directly associated with each Unit. Specifically you can assign a format consisting of a label and number of decimals for each unit. The actual formats are not present in this package but is provided in external unit packages such as uom-units.

import { Amount, Format } from "uom";
import { Units, UnitsFormat } from "uom-units";

const format = UnitFormat.getUnitFormat(Units.Meter, UnitsFormat);
console.log(
  "The amount is " +
    Math.round(Amount.valueAs(Units.Meter, amount), format.decimalCount) +
    " " +
    format.label
);

There is also the buildDerivedSymbol() function which will derive a symbol for a unit by looking at which base units the unit was created:

import { Amount, Format } from "uom";
import { Units } from "uom-units";

const length = Amount.create(10, Units.MeterPerSecond);
const label = Unit.buildDerivedSymbol(length);
console.log(label); // m/s

Serialization

This feature can be used to serialize the units for persisting to/from for example a database.

import { Amount, Serialize } from "uom";
import { Units } from "uom-units";

const length = Amount.create(10, Units.Meter);
const serialized = Serialize.amountToString(length);
const deserialized = Serialize.stringToAmount(serialized);

API

The API is divided into modules, where each module contains functions that operate on a type that is exported from that module. For example the Amount module exports the type Amount.Amount and has functions like Amount.plus().

For more information, see the full API docs.

How to develop

Create a PR for addition or changes. Please update the changelog's "unreleased" section with your changes in your PR.

How to publish

Don't forget to update the changelog before publishing. Then run:

yarn version --patch
yarn version --minor
yarn version --major

Prior art

This library was inspired by JSR-275. See also this repo, this article. Altough JSR-275 was not accepted it evolved into JSR-363 which is now accepted.

uom's People

Contributors

adamluotonen avatar bjolind avatar johankristiansson avatar jonaskello avatar jontem avatar marsve666 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

monerobby

uom's Issues

Release 2.0.0

So there has been some breaking changes merged into master lately. Most of them are just to undocumented API but still there are apps using these undocumented API so we need to increment the major version. The changes I know of are recorded in the change log. If anyone know of more changes that have been done please add them too.

So any more changes we want to do before releasing 2.0.0?

Unit registry

Today this package comes with a large set of different units. This causes a problem for some applications that wants to limit the number of units available in their application.

To solve this we could let the applications pick which units they want to work with. We could still include all the units in this package, but we need to provide a mechanism to work with a subset of the units.

For example, all units have meta-data which is registered in a global registry and this meta-data can then be used to fill UI drop-downs of available units. All units are automatically registered and available when this package is loaded. If we could disable this auto-registring and require an explicit call to register units that would give more control to applications to register the units they want to use and thus have control over what is displayed in the UI.

In addition we have some applications that use a different unit framework on the server-side and for those applications it is important that the units on the client side is the same as the supported units on the server-side.

All amounts are effectively the same type.

Due to Amount being a generic, with a type parameter T extends Quantity.Quantity, all Amount s are interchangeable.

This happens since T extends Quantity.Quantity won't give you one of the types in the union Quantity.Quantity, but all types that extends any of them.

https://github.com/dividab/uom/blob/master/src/amount.ts#L11

In essence, this is totally OK:

const foo: Amount.Amount<Quantity.Temperature> = Amount.create(0, Units.Hour);

It is also very deceptive, because if you remove the explicity typing of foo, its type is correct ( Amount.Amount<"Duration">), but that will still not prevent assigning it to the wrong type.

const foo = Amount.create(0, Units.Hour);
const bar: Amount.Amount<Quantity.Temperature> = foo;

References:

Clarify features

I think it would be good to clarify which features this package supports, both in the code and in the readme. Here is a list of the ones I know we use:

  • Conversion between units
  • Type-safety (declare function signatures to accept certain quantities)
  • Amount arithmetics (plus, minus, divide?, multiply?)
  • Create new unit (extensibility)
  • Serialize/Deserialize to a simple string ("Celcius")
  • Format unit name/symbol for on-screen display
  • Format unit value for on-screen display (number of decimals)
  • Switch between SI/IP and find corresponding unit
  • Put units in a dropdown (need to identify each unit as the selected one)

Serialize needs to be optimized

Each time stringToUnit is called, the string-to-unit map gets rebuilt with a lowercase key. When parsing a lot of propertyvaluesets, it adds up and becomes slow. Possible solution, the caller have responsibility of giving the unit map in with lower case keys.

Slow part

Identification of Units

From offline discussion with @AdamLuotonen:

Regarding Unit and Units ....

Problems:

  1. PoundLbPerPoundLb and KilogramPerKilogram, etc. are reduced down to the same unit (One) because units are always reduced to their simplest form. This leads to trouble identifying which unit the user wants to display. The same value is displayed, but the unit label should be different (kg/kg vs lb/lb).

  2. Known units vs. unknown units. We define a large amount of known units (MeterPerSecond, Percent, LiterPerMinute, etc.). However, the library allows creation of new units by the functions of unit-times.ts and unit-divide.ts. Units are dynamic and can be created in the applications (but it is not used a lot in practice). When serializing these new units they cannot be serialized in the form 123.22:CentiMeter because that only works with known units.

  3. Performance. Today, we send around instances of Unit everywhere, mainly in Amount objects. Unit contains a lot of information and may be a deep hierarchy describing how the unit is derived. When searching for the known name for the unit (for example, serialization) we run the following code:

export function getStringFromUnit (unit: Unit.Unit <Quantity>): string {
  _ensureMetaAdded ();
  const name = _unitToString [getUnitKey (unit)];
  if (name === undefined) {
    throw new error ("unknown unit" + unit);
  }
  return name;
}

uom/src/units.ts

Line 1382 in 9c7a38c

export function getStringFromUnit(unit: Unit.Unit<Quantity>): string {

function getUnitKey (unit: Unit.Unit <Quantity>): string {
  // Iterates whole array. ES6 does not have find
  const foundUnit = getAllUnits () .filter (u => Unit.equals (u, unit)) [0];
  if (! foundUnit) {
    throw new error ("Unknown Unit" + JSON.stringify (unit));
  }

  return JSON.stringify (foundUnit);
}

uom/src/units.ts

Line 1432 in 9c7a38c

function getUnitKey(unit: Unit.Unit<Quantity>): string {

getUnitKey is expensive (and therefore getStringFromUnit is also expensive) because we have to walk through all units and compare the structure. Even if we did not compare the structure it would be expensive, there is still a loop over a growing list of units. Running JSON.stringify is also not free (imagine thousands of calls to this).

Well. My suggestion is that instead of passing around the representation of the units in the application, I suggest that we only pass around the known unit names. When you want to do something with the unit, the library internally merges the unit structure into a map, and performs the same code as before. Serializing and deserializing becomes straightforward, a short string of unit name.

A potential problem with this may be that some applications may be dependent on Unit's internal structure. For example, Unit.quantity is available today, but it could be replaced by a single function call getQuantityForUnit(unit). Another potential issue may be if someone serializes and saves Unit with JSON.stringify, and then expects to be able to deserialize it and use it with the new code. There are some tests that suggest that:

  • Base unit One should be equal. Order should not matter
  • Alternate unit compare different object references

A bonus with this new approach is that you can easily see which unit a volume has when debugging. Instead of a complicated JSON structure, one gets a name.

Basically, the problem is that we want to identify units by name, but at the same time have a dynamic unit system that can build new units on the fly. I think it's enough that an application could record some own units at boot, but do not use dynamic units in the calculations?

Naming of unit name concepts

The unit today have several concepts to describe its name/identification:

  • name field in the Unit type - Used as internal lookup identity, and for serialization.
  • unit-name.ts module with getName function - Get name to display in UI, either derived or registered.
  • unit-name.ts module with registerLabel function - Used to override derived name.
  • symbol field in BaseUnit and AlternatveUnit type - used as fallback to build a derived name/label when no label is registered

To clarify what is what we should decide which concepts to keep and always use the same word to describe them.

  • Rename unit-name.ts module to unit-label.ts
  • Rename unit-name.ts module getName function to getLabel

For the symbol field we can either remove the function to have a fallback and default to blank when there is no registered label for a Unit, or we can rename this field to label.

Conditional types for UnitTimes and UnitDivide

While it is possible to generically divide any unit by any other unit and get a new unit that works for conversion, it is not possible to get the type of the new unit because we don't know which quantity it should get. To solve that UnitTimes and UnitDivide exists and they support dividing certain units by other units, and returning a correctly typed unit in the resulting quantity.

However it is still not possible to have a generic Amount.divide or Amount.times.

This might be solvable by using conditional types in typescript. Something like:

type Divide<Q1, Q2> = Q1 extends Length ? 
  Q2 extends Duration ? Velocity : 
  Q2 extends Length ? Dimensionless : never : never.

Make the tests data-driven

Many of the tests are the same code but different numbers. This can be rewritten so we separate out the data from the tests and replace all tests of the same type with a single test that loops on all the data items for that type of test.

Remove UnitFormat

It seems like UnitFormat has been refactored to the point where it bascially does nothing. Could it be removed?

The README has an example of using UnitFormat.getUnitFormat() that takes an Amount but this is not how the code actually works anymore:

This is the example:

const length = Amount.create(10, Units.Meter);
const format = UnitFormat.getUnitFormat(length);
console.log(
  "The amount is " +
    Math.round(Amount.valueAs(Units.Inch, amount), format.decimalCount) +
    " " +
    format.label
);

This is how the code works:

export function getUnitFormat(
  unit: Unit.Unit<unknown>,
  unitsFormat: UnitFormatMap
): UnitFormat | undefined {
  return unitsFormat[unit.name];
}

Function Format.getUnitsForQuantity should not assume native units

If you create your own unit of an existing quantity and a format for that unit you cannot use Format.getUnitsForQuantity since it assumes that you are using the native units.

Example:

const uom = require("uom");

const JohanPower = uom.Unit.timesNumber("JohanPower", 10000, uom.Units.Watt);
const JohanPowerFormat = uom.UnitFormat.createUnitFormat("JP", 0);

const myCustomUnits = {
  Watt: uom.Units.Watt,
  KiloWatt: uom.Units.KiloWatt,
  JohanPower: JohanPower
};

const myCustomUnitFormats = {
  Watt: uom.UnitsFormat.Meter,
  KiloWatt: uom.UnitsFormat.KiloWatt,
  JohanPower: JohanPowerFormat
};

const powerUnits = uom.Format.getUnitsForQuantity("Power", myCustomUnitFormats); // Crash

Instead it should accept a paramter of units, which could be defaulted to the native ones.

Don't publish sourcemap references

We do not publish sourcemap files to npm but the js files still reference source maps which causes problems in applications which use webpack source-map-loader.

Request for new units

Would like to have the units StandardCubicMeterPerHourPerSquareMeter and StandardCubicFootPerSquareFeet added as IMassFlowPerArea

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.