Skip to content

Commit

Permalink
Docs (#117)
Browse files Browse the repository at this point in the history
* Starting things out

* Docs fixes and style updates

* Update travis.yml

* \n
  • Loading branch information
jscheiny authored Feb 14, 2019
1 parent 09cb037 commit 0187f17
Show file tree
Hide file tree
Showing 21 changed files with 1,512 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

### Build
dist/
build/
coverage/
*.pyc

Expand Down
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ coverage/
codegen/
scripts/
test/
docs/
docsgen/
jest.config.json
.travis.yml
tslint.json
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ script:
- yarn verify
notifications:
email: false
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
keep-history: true
local-dir: docs/build
committer-from-gh: true
on:
branch: master
71 changes: 71 additions & 0 deletions docs/builtin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Built-in Units

While users can define their own unit system (see [Defining Quantities](defining-quantities.html)), Safe Units also comes with a large collection of built-in quantities and units based on the SI unit system.

All quantities provided are generic, of the form `type Quantity<N = number>` so all will default to using `number` as the numeric type but may be passed another numeric type.

## Base Units

The built-in units include the standard set of SI base dimension and corresponding units:

* `Length` / `meters`
* `Mass` / `kilograms`
* `Time` `seconds`
* `ElectricCurrent` / `amperes`
* `Temperature` / `kelvin`
* `AmountOfSubstance` / `moles`
* `LuminousIntensity` / `candelas`

In addition, several extra dimensions are defined:

* `PlaneAngle` / `radians`
* `SolidAngle` / `steradians`
* `Memory` / `bits`

While plane and solid angles are defined as dimensionless in the SI specification, they are defined here in case users want to be more rigorous with their angles. If not, then these can be safely ignored.

## Generic Base Units

The base units are only provided for `number` measures, but users can create the base units for a given measure type through:

```ts
type SpecialNumber = /* ... */;
const numericOps: INumericOperations<SpecialNumber> = {/* ... */};
const SpecialMeasure = createMeasureType(numericOps);
const {
meters,
kilograms,
seconds,
amperes,
kelvin,
moles,
candelas,
radians,
steradians,
bits,
} = createBaseUnits(SpecialMeasure);
```

These versions of `meters`, `kilograms`, etc. will all operator on measures whose numeric types are `SpecialNumber`.

## Provided Units

A large number of built in quantities (e.g. distance, velocity, force and magnetic flux density) are provided. See [here](https://github.com/jscheiny/safe-units/blob/master/src/unit/quantities.ts) for a full list.

Many units are provided from the metric, Imperial and U.S. customary unit systems. See [here](https://github.com/jscheiny/safe-units/tree/master/src/unit) for all of the units.

Prefix functions are provided for both standard [SI prefixes](https://github.com/jscheiny/safe-units/blob/master/src/unit/metric.ts) and for [memory prefixes](https://github.com/jscheiny/safe-units/blob/master/src/unit/memory.ts).

Trigonometric functions are provided in the `Trig` namespace for converting between plane angles and dimensionless values, the signatures are as follows:

```ts
namespace Trig {
function cos(x: PlaneAngle): Dimensionless;
function sin(x: PlaneAngle): Dimensionless;
function tan(x: PlaneAngle): Dimensionless;
function acos(x: Dimensionless): PlaneAngle;
function asin(x: Dimensionless): PlaneAngle;
function atan(x: Dimensionless): PlaneAngle;
function atan2(y: Length, x: Length): PlaneAngle;
}
```
109 changes: 109 additions & 0 deletions docs/defining-quantities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Defining Quantities

We define a **quantity** as the type of a measurement. This includes things like length, time, force, velocity, and pressure. Safe Units contains many built in quantities so we can write typesafe code simply:

```ts
import { Acceleration, Time, Velocity } from "safe-units";

function computeAcceleration(speed: Velocity, time: Time): Acceleration {
return speed.over(time);
}
```

## Dimensions and Base Units

A **dimension** is the basis of a unit system. Units are composed of multiplying together multiple dimensions. For example, in the SI unit system there are the following dimensions, each corresponding to a base unit of the system

- Length - meters (m)
- Mass - kilograms (kg)
- Time - seconds (s)
- Electric Current - amperes (A)
- Temperature - kelvin (K)
- Amount of substance - moles (mol)
- Luminous intensity - candelas (cd)

We can use the `Measure.dimension` static method to define base units for a given dimension. Here's how Safe Units defines the dimensions and base units for length and time:

```ts
const meters = Measure.dimension("length", "m");
const Length = meters;
type Length = typeof meters;

const seconds = Measure.dimension("time", "s");
const Time = seconds;
type Time = typeof seconds;
```

Let's break this down:

```ts
const meters = Measure.dimension("length", "m");
```
The first argument should be a unique string literal that defines the name of the dimension that we are creating. Note that if two calls to `Measure.dimension` are made with the same first argument, they will be considered the same dimension. The second argument is the symbol for the base unit of that dimension. This returns a `Measure` that represents 1 base unit (in the example above 1 meter and 1 second).

```ts
type Length = typeof meters;
```
Next, we create a type for the quantity that this unit represents. This type is useful for specifying the types of values we use later on (e.g. `const l: Length = Measure.of(30, feet)`). As noted above, a quantity is just the type of a measure so we define just that way.

```ts
const Length = meters;
```
Finally, we create a value for the quantity. This is very useful for expressively deriving more quantities from this base unit as can be seen below.

## Derived Quantities

Now that we have a set of base units and dimensions, we can derive more interesting quantities by using the quantity values we defined just above. Let's create velocity and acceleration quantities:

```ts
const Velocity = Length.over(Time);
type Velocity = typeof Velocity;

const Acceleration = Velocity.over(Time);
type Acceleration = typeof Acceleration;
```

Now we can see how the values for `Length` and `Time` came in handy for writing this nicely. If we didn't have values for this we would need to define velocity as:

```ts
const Velocity = meters.over(seconds);
```

While not technically wrong, `Length.over(Time)` is a much cleaner definition of velocity.

It's worth noting that since our quantities defined thus far are instances of `Measure`, we can perform all the same operations on `Measures` on quantities. For example, here is how Safe Units defines the quantity of magnetic flux density:

```ts
const MagneticFluxDensity = Voltage.times(Time).over(Area);
```

This can then mirror how this measure might be computed:

```ts
function computeMagneticFluxDensity(
voltage: Voltage,
time: Time,
area: Area,
): MagneticFluxDensity {
return voltage.times(time).over(area);
}
```

## Generic Quantities

The examples in here are for creating dimensions and quantities for the built in `Measure` type. However, measures can be made for any numeric type (see [Generic Measures](generic-measures.html)). As such we may want to make our quantities generic as well. This can be easily done with the generic `LiftMeasure` type which takes a measure of a given unit and changes its numeric type.

For example, suppose we have a `WrappedNumber` type, we can change a `Velocity` quantity from operating on number measures to `WrappedNumber` measures as follows:

```ts
type WrappedVelocity = LiftMeasure<Velocity, WrappedNumber>
```
We could also just define the `Velocity` type to be generic in the first place. The recommended way of doing this is as follows:
```ts
const Velocity = Length.over(Time);
type Velocity<N = number> = LiftMeasure<typeof Velocity, N>;
```

This way, we can use `Velocity` to refer to the quantity on numbers, but also use `Velocity<WrappedNumber>` to operate on `WrappedMeasure`s.
79 changes: 79 additions & 0 deletions docs/generic-measures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Generic Measures

The default Safe Units `Measure` class is specifically designed around using measures where the underlying values are represented by the JavaScript `number` type. However, it wouldn't be unreasonable to want measures where the values have different requirements. For example, one might want measures with arbitrary precision or measures on the rational numbers. Creating such a measure class can be accomplished by using the `createMeasureType` function.

## Example

Suppose we have our own number type:

```ts
class WrappedNumber {
constructor(public readonly value: number) {}

foo(): WrappedNumber { ... }
}

function wrap(value: number) {
return new WrappedNumber(value);
}
```

Now we're going to create our own measure type that operates on `WrappedNumber`s, let's call it `WrappedMeasure`:

```ts
import { createMeasureType, GenericMeasure, Unit } from "safe-units";

type WrappedMeasure<U extends Unit> = IGenericMeasure<WrappedNumber, U>;
const WrappedMeasure = createMeasureType({
one: () => wrap(1),
neg: x => wrap(-x),
add: (x, y) => wrap(x.value + y.value),
sub: (x, y) => wrap(x.value - y.value),
mult: (x, y) => wrap(x.value * y.value),
div: (x, y) => wrap(x.value / y.value),
pow: (x, y) => wrap(x.value ** y),
compare: (x, y) => x - y,
format: x => `${x}`,
});
```

We can then use this class just as we would use `Measure`, except anywhere we'd expect a `number` we now expect a `WrappedNumber`.

## Breakdown

Let's deconstruct this example to explain what's going on. First we start with this type definition:

```ts
type WrappedMeasure<U extends Unit> = IGenericMeasure<WrappedNumber, U>;
```

This line isn't strictly necessary, but it is often useful to have our `WrappedMeasure` available as a type. The next line `const WrappedMeasure = ...` creates a value for wrapped measures. Having a type for the measure is useful for writing generic functions on wrapped measures. All this line does is bind the numeric type of `GenericMeasure`. Similarly, the `Measure` type has the following definition:

```ts
type Measure<U extends Unit> = IGenericMeasure<number, U>;
```

After we've defined the type of `WrappedMeasure` we now define the class itself by calling `createMeasureType`. This function takes an object which let's the generic measure type know how to perform operations on our numeric type. Note that for this simple example, we generally just unwrap the value, perform the arithmetic operation and then wrap it back up. Most of these operations should be self-explanatory, however some require some further explanation:

- `one`: A function with no arguments that simply returns the 1 value or multiplicative identity of your number system. This is used to construct base units whose values are implicitly one.
- `pow`: This function is slightly different from the rest of the arithmetic operations in that it doesn't take to values of type `N`, instead its signature is: `pow: (base: N, power: Exponent) => N` where `Exponent` is the union of `-5 | -4 | ... | 4 | 5`. This is due to the computational limitations of the library that we need to be specific in the kinds of exponents we can handle.
- `compare`: A function that returns a negative `number` if its first argument is less than its second, a positive `number` if its first argument is greater than its second, and `0` if the arguments are equal.

## Usage

The returned `WrappedMeasure` now behaves just like `Measure` does except in the domain of wrapped numbers. This means we can call `WrappedMeasure.dimension` or `WrappedMeasure.of` just as expected. It's important to note that the provided quantities and units (`Length`, `Time`, `meters`, `seconds`, etc) will need to be redefined for other measure types as these types are all specific to `number`s.

## Static Methods

By default, generic measures come with a set of static methods that can be applied to measures of all numeric types. However, certain static methods may only make sense for a given numeric type. For example the `Measure.trunc` method exists because `Math.trunc` applies to `number`s. To add static methods to a generic measure type, simple pass an object as a second argument to `createMeasureType`:

```ts
declare function foo(value: WrappedNumber): WrappedNumber;
declare const mass: WrappedMeasure<{ mass: 1 }>;

const WrappedMeasure = createMeasureType({ ... }, {
foo: wrapUnaryFn(foo),
});

WrappedMeasure.foo(mass);
```
37 changes: 37 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Safe Units

Safe Units is a library for using units of measurement in TypeScript in a type safe manner. Check it out on [github](https://github.com/jscheiny/safe-units). Safe Units provides an implementation of an SI based unit system but is flexible enough to allow users to create their own unit systems which can be independent or can interoperate with the built-in units. Users can also make unit systems for any numeric type they'd like not just the JavaScript `number` type. This library makes heavy use of conditional types and thus requires TypeScript 2.9+.

```ts
import { Length, Measure, meters, seconds, Time, Velocity } from "safe-units";

const length: Length = Measure.of(30, meters);
const time: Time = Measure.of(15, seconds);
const velocity: Velocity = length.over(time);

console.log(length.toString()); // 30 m
console.log(time.toString()); // 15 s
console.log(velocity.toString()); // 2 m * s^-1

const error: Velocity = length.times(time); // Error: A measure of m*s isn't assignable to a measure of m/s.
```

## Prerequisites

Safe units is written and TypeScript and should be consumed by TypeScript users to take full advantage of what it provides. In addition you will need the following:

- TypeScript 2.9 or later
- Strict null checks enabled for your project

## Installation

```
npm install safe-units
```

or

```
yarn add safe-units
```

34 changes: 34 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Limitations

Since Safe Units is typesafe, it must compute units at compile time. Due to some technical limitations of what you can do with types in TypeScript, there is one major limitation to this library: The exponents for dimensions of your units are limited to integers between -5 and 5 (inclusive). This means that you can not represent a value of 30 m<sup>6</sup> in this library (though, why would you?).

In the research I've conducted for this library I cannot find any instances in which it would be useful to use units with such extreme exponents. If you're aware of any such use cases, please [file an issue](https://github.com/jscheiny/safe-units/issues/new) to discuss it.

The result of this limitation is that certain operations may fail somewhat unexpectedly. With any luck, these should never occur in practice, but just in case, here's a list of what can fail:

## Multiplication / Division

If two units will multiply or divide together to create an exponent out of range a compile error will occur. This will fail at compile time by marking the argument as invalid.

```ts
const a = Measure.of(1, meters.cubed());
const b = Measure.of(1, meters.toThe(-3));
const product = a.times(a);
// ~ Error: The result would have unit m^6
const quotient = a.over(b);
// ~ Error: The result would have unit m^-6
```

## Exponentiation

The `squared` and `cubed` methods of measure will fail when applied to measures that would create invalid results. This will fail at compile time because for measures which cannot be squared or cubed these methods will have type `never`. The `toThe` method of measure will fail at compile time by marking the argument as invalid.

```ts
const m = Measure.of(30, meters.cubed());
const a = m.squared();
// ~~~~~~~~~~~ Error: squared has type never
const b = m.cubed();
// ~~~~~~~~~ Error: cubed has type never
const c = m.toThe(3);
// ~ Error: m cannot be cubed so 3 is an invalid argument
```
Loading

0 comments on commit 0187f17

Please sign in to comment.