Skip to content

Commit

Permalink
SafeMath namespace (#27)
Browse files Browse the repository at this point in the history
* Pull out math function into separate file

* Separate math tests file

* Export safe math

* Update readme

* Remove trig functions
  • Loading branch information
jscheiny authored May 30, 2018
1 parent 0d56578 commit e6065f2
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 152 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ const error: Velocity = length.times(time); // Error: A measure of m*s isn't ass
### Measure arithmetic

```typescript
import { Area, Force, Length, Mass, Measure, Pressure, Quantity, Unit, Volume } from "uni-ts";
import { Area, Force, Length, Mass, Measure, Pressure, Quantity, SafeMath, Unit, Volume } from "uni-ts";

const length: Length = Measure.of(30, Unit.feet);
const width: Length = Measure.of(20, Unit.miles);
const height: Length = Measure.of(10, Unit.meters);
const area: Area = length.times(width);
const squareSide: Length = Measure.sqrt(area);
const squareSide: Length = SafeMath.sqrt(area);
const volume: Volume = area.times(height);
const perimeter: Length = length.scale(2).plus(width.scale(2));
const mass: Mass = Measure.of(100, Unit.pounds);
Expand All @@ -51,13 +51,15 @@ const pressure: Pressure = force.over(area);
### Type errors

```typescript
import { Length, Force, Measure, SafeMath } from "uni-ts";

const length: Length = Measure.of(10, Unit.meters);
const time: Time = Measure.of(10, Unit.seconds);
length.plus(time); // Error: Measures of different units cannot be added
length.minus(time); // Error: Measures of different units cannot be subtracted

const force: Force = length.over(time) // Error: Measure of m/s is not assignable to measure of kg*m/s^2
const root = Measure.sqrt(length) // Error: Can't take sqrt of measure of m since it's not a perfect square
const root = SafeMath.sqrt(length) // Error: Can't take sqrt of measure of m since it's not a perfect square
```

### Naming units
Expand Down
62 changes: 62 additions & 0 deletions src/measure/__test__/mathTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { meters, seconds } from "../../unit";
import * as SafeMath from "../math";
import { Measure } from "../measure";

describe("Math", () => {
const mps = meters.per(seconds);

it("arithmetic", () => {
expect(SafeMath.add(Measure.of(5, mps), Measure.of(-5, mps))).toEqual(Measure.of(0, mps));
expect(SafeMath.subtract(Measure.of(5, mps), Measure.of(-5, mps))).toEqual(Measure.of(10, mps));
expect(SafeMath.multiply(Measure.of(5, mps), Measure.of(10, seconds))).toEqual(Measure.of(50, meters));
expect(SafeMath.divide(Measure.of(50, meters), Measure.of(10, seconds))).toEqual(Measure.of(5, mps));
});

it("abs", () => {
expect(SafeMath.abs(Measure.of(-10, mps))).toEqual(Measure.of(10, mps));
});

it("cbrt", () => {
expect(SafeMath.cbrt(Measure.of(64, seconds.cubed()))).toEqual(Measure.of(4, seconds));
});

it("ceil", () => {
expect(SafeMath.ceil(Measure.of(3.4, mps))).toEqual(Measure.of(4, mps));
});

it("floor", () => {
expect(SafeMath.floor(Measure.of(7.8, mps))).toEqual(Measure.of(7, mps));
});

it("hypot", () => {
expect(SafeMath.hypot(Measure.of(3, meters), Measure.of(4, meters))).toEqual(Measure.of(5, meters));
});

it("max", () => {
expect(SafeMath.max(Measure.of(10, mps), Measure.of(5, mps), Measure.of(15, mps))).toEqual(Measure.of(15, mps));
});

it("min", () => {
expect(SafeMath.min(Measure.of(10, mps), Measure.of(5, mps), Measure.of(15, mps))).toEqual(Measure.of(5, mps));
});

it("pow", () => {
expect(SafeMath.pow(Measure.of(10, meters), 3)).toEqual(Measure.of(1000, meters.cubed()));
});

it("round", () => {
expect(SafeMath.round(Measure.of(7.8, mps))).toEqual(Measure.of(8, mps));
});

it("sqrt", () => {
expect(SafeMath.sqrt(Measure.of(25, meters.squared()))).toEqual(Measure.of(5, meters));
});

it("sum", () => {
expect(SafeMath.sum(Measure.of(10, mps), Measure.of(5, mps), Measure.of(15, mps))).toEqual(Measure.of(30, mps));
});

it("trunc", () => {
expect(SafeMath.trunc(Measure.of(-7.8, mps))).toEqual(Measure.of(-7, mps));
});
});
77 changes: 2 additions & 75 deletions src/measure/__test__/measureTests.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { meters, seconds } from "../../unit";
import { cubic, Measure, square } from "../measure";
import { Unit } from "../units";

describe("Measures", () => {
const meter = Measure.dimension("L");
const second = Measure.dimension("T");
const mps = meter.per(second);
const mps2 = mps.per(second);

class TestMeasure<U extends Unit> extends Measure<Unit> {
constructor(value: number, unit: U, symbol?: string | undefined) {
super(value, unit, symbol);
}
}

describe("dimension", () => {
it("should create dimensions with value 1", () => {
expect(Measure.dimension("foo")).toEqual({ value: 1, unit: { foo: 1 } });
Expand Down Expand Up @@ -170,7 +162,7 @@ describe("Measures", () => {
describe("formatting", () => {
it("should format dimensionless units", () => {
expect(Measure.dimensionless(10).toString()).toBe("10");
expect(new TestMeasure(10, { x: 0, y: undefined }).toString()).toBe("10");
expect(Measure.unsafeConstruct(10, { x: 0, y: undefined }).toString()).toBe("10");
});

it("should format base units", () => {
Expand Down Expand Up @@ -211,7 +203,7 @@ describe("Measures", () => {
});

it("should skip formatting explicitly 0 and undefined dimension", () => {
expect(new TestMeasure(10, { x: 0, y: undefined, z: 2 }).toString()).toBe("10 z^2");
expect(Measure.unsafeConstruct(10, { x: 0, y: undefined, z: 2 }).toString()).toBe("10 z^2");
});

it("should format measures as other measures with symbols", () => {
Expand All @@ -232,69 +224,4 @@ describe("Measures", () => {
expect(Measure.of(1, m.squared().per(s.squared())).toString()).toBe("1 meter^2 * second^-2");
});
});

describe("math", () => {
const mps = meters.per(seconds);

it("arithmetic", () => {
expect(Measure.add(Measure.of(5, mps), Measure.of(-5, mps))).toEqual(Measure.of(0, mps));
expect(Measure.subtract(Measure.of(5, mps), Measure.of(-5, mps))).toEqual(Measure.of(10, mps));
expect(Measure.multiply(Measure.of(5, mps), Measure.of(10, seconds))).toEqual(Measure.of(50, meters));
expect(Measure.divide(Measure.of(50, meters), Measure.of(10, seconds))).toEqual(Measure.of(5, mps));
});

it("abs", () => {
expect(Measure.abs(Measure.of(-10, mps))).toEqual(Measure.of(10, mps));
});

it("cbrt", () => {
expect(Measure.cbrt(Measure.of(64, seconds.cubed()))).toEqual(Measure.of(4, seconds));
});

it("ceil", () => {
expect(Measure.ceil(Measure.of(3.4, mps))).toEqual(Measure.of(4, mps));
});

it("floor", () => {
expect(Measure.floor(Measure.of(7.8, mps))).toEqual(Measure.of(7, mps));
});

it("hypot", () => {
expect(Measure.hypot(Measure.of(3, meters), Measure.of(4, meters))).toEqual(Measure.of(5, meters));
});

it("max", () => {
expect(Measure.max(Measure.of(10, mps), Measure.of(5, mps), Measure.of(15, mps))).toEqual(
Measure.of(15, mps),
);
});

it("min", () => {
expect(Measure.min(Measure.of(10, mps), Measure.of(5, mps), Measure.of(15, mps))).toEqual(
Measure.of(5, mps),
);
});

it("pow", () => {
expect(Measure.pow(Measure.of(10, meters), 3)).toEqual(Measure.of(1000, meters.cubed()));
});

it("round", () => {
expect(Measure.round(Measure.of(7.8, mps))).toEqual(Measure.of(8, mps));
});

it("sqrt", () => {
expect(Measure.sqrt(Measure.of(25, meters.squared()))).toEqual(Measure.of(5, meters));
});

it("sum", () => {
expect(Measure.sum(Measure.of(10, mps), Measure.of(5, mps), Measure.of(15, mps))).toEqual(
Measure.of(30, mps),
);
});

it("trunc", () => {
expect(Measure.trunc(Measure.of(-7.8, mps))).toEqual(Measure.of(-7, mps));
});
});
});
3 changes: 2 additions & 1 deletion src/measure/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as SafeMath from "./math";
import { Unit } from "./units";

export { Unit };
export { SafeMath, Unit };
export * from "./measure";
72 changes: 72 additions & 0 deletions src/measure/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Exponent } from "../exponent";
import { Measure } from "./measure";
import { DivideUnits, ExponentiateUnit, MultiplyUnits, NthRootableUnit, NthRootUnit } from "./types";
import { nthRootUnit, Unit } from "./units";

export const abs = wrapUnary(Math.abs);
export const ceil = wrapUnary(Math.ceil);
export const floor = wrapUnary(Math.floor);
export const fround = wrapUnary(Math.fround);
export const hypot = warpNary(Math.hypot);
export const max = warpNary(Math.max);
export const min = warpNary(Math.min);
export const round = wrapUnary(Math.round);
export const trunc = wrapUnary(Math.trunc);

export function pow<U extends Unit, Y extends Exponent>(x: Measure<U>, y: Y): Measure<ExponentiateUnit<U, Y>> {
return x.toThe(y);
}

export function sqrt<U extends NthRootableUnit<2>>(x: Measure<U>): Measure<NthRootUnit<U, 2>> {
return Measure.unsafeConstruct(Math.sqrt(x.value), nthRootUnit(x.getUnit(), 2));
}

export function cbrt<U extends NthRootableUnit<3>>(x: Measure<U>): Measure<NthRootUnit<U, 3>> {
return Measure.unsafeConstruct(Math.cbrt(x.value), nthRootUnit(x.getUnit(), 3));
}

export function add<U extends Unit>(left: Measure<U>, right: Measure<U>): Measure<U> {
return left.plus(right);
}

export function subtract<U extends Unit>(left: Measure<U>, right: Measure<U>): Measure<U> {
return left.minus(right);
}

export function multiply<L extends Unit, R extends Unit>(
left: Measure<L>,
right: Measure<R>,
): Measure<MultiplyUnits<L, R>> {
return left.times(right);
}

export function divide<L extends Unit, R extends Unit>(
left: Measure<L>,
right: Measure<R>,
): Measure<DivideUnits<L, R>> {
return left.over(right);
}

export function sum<U extends Unit>(first: Measure<U>, ...rest: Array<Measure<U>>): Measure<U> {
let result = first;
for (const measure of rest) {
result = result.plus(measure);
}
return result;
}

function wrapUnary(f: (x: number) => number) {
return <U extends Unit>(x: Measure<U>): Measure<U> => {
return Measure.of(f(x.value), x.normalized());
};
}

function warpNary(f: (...x: number[]) => number) {
return <U extends Unit>(first: Measure<U>, ...rest: Array<Measure<U>>): Measure<U> => {
return Measure.of(f(...values(first, ...rest)), first.normalized());
};
}

function values<U extends Unit>(...measures: Array<Measure<U>>): number[] {
return measures.map(measure => measure.value);
}
80 changes: 7 additions & 73 deletions src/measure/measure.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,10 @@
import { Exponent } from "../exponent";
import { Dimensionless } from "../quantity";
import { formatUnit, setDimensionSymbol } from "./format";
import { DivideUnits, ExponentiateUnit, MultiplyUnits, NthRootableUnit, NthRootUnit } from "./types";
import { dimension, divideUnits, exponentiateUnit, multiplyUnits, nthRootUnit, Unit } from "./units";
import { DivideUnits, ExponentiateUnit, MultiplyUnits } from "./types";
import { dimension, divideUnits, exponentiateUnit, multiplyUnits, Unit } from "./units";

export class Measure<U extends Unit> {
// Math functions

public static abs = wrapUnary(Math.abs);
public static ceil = wrapUnary(Math.ceil);
public static floor = wrapUnary(Math.floor);
public static fround = wrapUnary(Math.fround);
public static hypot = warpNary(Math.hypot);
public static max = warpNary(Math.max);
public static min = warpNary(Math.min);
public static round = wrapUnary(Math.round);
public static trunc = wrapUnary(Math.trunc);

public static pow<U extends Unit, Y extends Exponent>(x: Measure<U>, y: Y): Measure<ExponentiateUnit<U, Y>> {
return x.toThe(y);
}

public static sqrt<U extends NthRootableUnit<2>>(x: Measure<U>): Measure<NthRootUnit<U, 2>> {
return new Measure(Math.sqrt(x.value), nthRootUnit(x.unit, 2));
}

public static cbrt<U extends NthRootableUnit<3>>(x: Measure<U>): Measure<NthRootUnit<U, 3>> {
return new Measure(Math.cbrt(x.value), nthRootUnit(x.unit, 3));
}

public static add<U extends Unit>(left: Measure<U>, right: Measure<U>): Measure<U> {
return left.plus(right);
}

public static subtract<U extends Unit>(left: Measure<U>, right: Measure<U>): Measure<U> {
return left.minus(right);
}

public static multiply<L extends Unit, R extends Unit>(
left: Measure<L>,
right: Measure<R>,
): Measure<MultiplyUnits<L, R>> {
return left.times(right);
}

public static divide<L extends Unit, R extends Unit>(
left: Measure<L>,
right: Measure<R>,
): Measure<DivideUnits<L, R>> {
return left.over(right);
}

public static sum<U extends Unit>(first: Measure<U>, ...rest: Array<Measure<U>>): Measure<U> {
let result = first;
for (const measure of rest) {
result = result.plus(measure);
}
return result;
}

// Construction functions

public static dimension<Dimension extends string>(dim: Dimension, symbol?: string) {
Expand All @@ -76,9 +22,13 @@ export class Measure<U extends Unit> {
return new Measure(value * quantity.value, quantity.unit, symbol);
}

public static unsafeConstruct<U extends Unit>(value: number, unit: U, symbol?: string): Measure<U> {
return new Measure(value, unit, symbol);
}

// Instance methods

protected constructor(
private constructor(
public readonly value: number,
private readonly unit: U,
private readonly symbol?: string | undefined,
Expand Down Expand Up @@ -203,19 +153,3 @@ export function square<U extends Unit>(measure: Measure<U>): Measure<Exponentiat
export function cubic<U extends Unit>(measure: Measure<U>): Measure<ExponentiateUnit<U, 3>> {
return measure.cubed();
}

function wrapUnary(f: (x: number) => number) {
return <U extends Unit>(x: Measure<U>): Measure<U> => {
return Measure.of(f(x.value), x.normalized());
};
}

function warpNary(f: (...x: number[]) => number) {
return <U extends Unit>(first: Measure<U>, ...rest: Array<Measure<U>>): Measure<U> => {
return Measure.of(f(...values(first, ...rest)), first.normalized());
};
}

function values<U extends Unit>(...measures: Array<Measure<U>>): number[] {
return measures.map(measure => measure.value);
}

0 comments on commit e6065f2

Please sign in to comment.