Skip to content

Commit

Permalink
Unit systems (take 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
jscheiny committed May 17, 2024
1 parent 13604d3 commit de687d4
Show file tree
Hide file tree
Showing 26 changed files with 574 additions and 474 deletions.
8 changes: 7 additions & 1 deletion src/measure/__test__/genericMeasureTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { NumericOperations } from "../genericMeasure";
import { createMeasureType } from "../genericMeasureFactory";
import { wrapBinaryFn, wrapReducerFn, wrapSpreadFn, wrapUnaryFn } from "../genericMeasureUtils";
import { Measure } from "../numberMeasure";
import { UnitSystem } from "../unitSystem";

describe("Generic measures", () => {
const unitSystem = UnitSystem.from({
length: "m",
mass: "kg",
});

describe("function wrappers", () => {
const meters = Measure.dimension("L", "m");
const meters = Measure.dimension(unitSystem, "length", "m");
const add = (left: number, right: number) => left + right;

it("unary wrapper", () => {
Expand Down
40 changes: 22 additions & 18 deletions src/measure/__test__/numberMeasureTests.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { MeasureFormatter } from "../genericMeasure";
import { Measure } from "../numberMeasure";
import { UnitSystem } from "../unitSystem";

describe("Number measures", () => {
const meters = Measure.dimension("length", "m");
const seconds = Measure.dimension("time", "s");
const kilograms = Measure.dimension("mass", "kg");
const unitSystem = UnitSystem.from({
length: "m",
time: "s",
mass: "kg",
});

const meters = Measure.dimension(unitSystem, "length");
const seconds = Measure.dimension(unitSystem, "time");
const kilograms = Measure.dimension(unitSystem, "mass");
const mps = meters.per(seconds);
const mps2 = mps.per(seconds);

describe("dimension", () => {
it("should create dimensions with value 1", () => {
expect(Measure.dimension("foo", "f")).toEqual({ value: 1, unit: { foo: ["f", 1] }, symbol: "f" });
expect(meters).toEqual({
value: 1,
unit: { length: 1, mass: 0, time: 0 },
symbol: "m",
unitSystem: unitSystem,
});
});
});

Expand All @@ -29,9 +41,9 @@ describe("Number measures", () => {
});

it("should construct dimensionless values", () => {
const dimensionless = Measure.dimensionless(3);
const dimensionless = Measure.dimensionless(unitSystem, 3);
expect(dimensionless.value).toBe(3);
expect(dimensionless.unit).toEqual({});
expect(dimensionless.unit).toEqual({ length: 0, time: 0, mass: 0 });
});
});

Expand Down Expand Up @@ -223,12 +235,12 @@ describe("Number measures", () => {
});

describe("formatting", () => {
function expectFormat(unit: Measure<any>, formatted: string, formatter?: MeasureFormatter<number>): void {
function expectFormat(unit: Measure<any, any>, formatted: string, formatter?: MeasureFormatter<number>): void {
expect(unit.toString(formatter)).toBe(formatted);
}

it("should format dimensionless units", () => {
expectFormat(Measure.dimensionless(10), "10");
expectFormat(Measure.dimensionless(unitSystem, 10), "10");
});

it("should format base units", () => {
Expand Down Expand Up @@ -274,7 +286,7 @@ describe("Number measures", () => {

it("should not format using symbol even if present", () => {
expect(Measure.of(5, meters.squared()).withSymbol("m2").toString()).toBe("5 m^2");
expect(Measure.dimensionless(0).withSymbol("rad").toString()).toBe("0");
expect(Measure.dimensionless(unitSystem, 0).withSymbol("rad").toString()).toBe("0");
});

it("should format measures as other measures with symbols", () => {
Expand All @@ -293,14 +305,6 @@ describe("Number measures", () => {
expect(Measure.of(500, meters).valueIn(kilometers)).toBe(0.5);
});

it("should use base unit symbols to format when available", () => {
const m = Measure.dimension("test-length", "meter");
const s = Measure.dimension("test-time", "second");
expect(m.toString()).toBe("1 meter");
expect(Measure.of(1, m.per(s)).toString()).toBe("1 meter / second");
expect(Measure.of(1, m.squared().per(s.squared())).toString()).toBe("1 meter^2 / second^2");
});

it("should use a custom formatter for values if provided", () => {
expectFormat(Measure.of(3.14159, meters), "3.14 m", {
formatValue: value => value.toPrecision(3),
Expand Down Expand Up @@ -353,7 +357,7 @@ describe("Number measures", () => {
const valueMapped = original.unsafeMap(value => value + 1);
const unitMapped = original.unsafeMap(
value => value + 1,
unit => ({ ...unit, time: ["s", -1] }),
unit => ({ ...unit, time: -1 as const }),
);
expect(valueMapped).toEqual(Measure.of(2, meters));
expect(unitMapped).toEqual(Measure.of(2, meters.per(seconds)));
Expand Down
81 changes: 81 additions & 0 deletions src/measure/__test__/unitSystemTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { UnitSystem } from "../unitSystem";

describe("Unit system", () => {
describe("Basic operations", () => {
const unitSystem = UnitSystem.from({ length: "m", mass: "kg", time: "s" });
it("it should return the dimensions of the basis", () => {
expect(unitSystem.getDimensions()).toEqual(["length", "mass", "time"]);
});

it("it should return the symbol of a dimension", () => {
expect(unitSystem.getSymbol("length")).toEqual("m");
});

it("it should create a dimensionless unit", () => {
expect(unitSystem.createDimensionlessUnit()).toEqual({ length: 0, mass: 0, time: 0 });
});

it("it should throw an error when trying to get the symbol of a dimension that doesn't exist", () => {
expect(() => unitSystem.getSymbol("memory" as any)).toThrow("No symbol found for dimension: memory");
});

it("it should create a dimension unit", () => {
expect(unitSystem.createDimensionUnit("length")).toEqual({ length: 1, mass: 0, time: 0 });
});
});

describe("arithmetic", () => {
const unitSystem = UnitSystem.from({
w: "w",
x: "x",
y: "y",
z: "z",
});

const x = unitSystem.createDimensionUnit("x");
const y = unitSystem.createDimensionUnit("y");
const dimensionless = unitSystem.createDimensionlessUnit();

describe("multiplication", () => {
it("should multiply two different base units correctly", () => {
expect(unitSystem.multiply(x, y)).toEqual({ ...dimensionless, x: 1, y: 1 });
});

it("should multiply two of the same base unit correctly", () => {
expect(unitSystem.multiply(x, x)).toEqual({ ...dimensionless, x: 2 });
});

it("should multiply complex units correctly", () => {
const left = { ...dimensionless, x: 1, y: -2 };
const right = { ...dimensionless, y: 1, z: 2 };
expect(unitSystem.multiply(left, right)).toEqual({ ...dimensionless, x: 1, y: -1, z: 2 });
});

it("should handle explicitly 0 exponents", () => {
const left = { ...dimensionless, w: 0, x: 2 };
const right = { ...dimensionless, y: 0 };
expect(unitSystem.multiply(left, right)).toEqual({ ...dimensionless, x: 2 });
});
});

describe("reciprocals", () => {
it("should get the reciprocal of a unit", () => {
const unit = { ...dimensionless, x: 1, y: -2, z: 3 };
expect(unitSystem.reciprocal(unit)).toEqual({ ...dimensionless, x: -1, y: 2, z: -3 });
});

it("should handle explicitly 0 exponents", () => {
const unit = { ...dimensionless, w: 0, x: 2 };
expect(unitSystem.reciprocal(unit)).toEqual({ ...dimensionless, x: -2 });
});
});

describe("division", () => {
it("should correctly divide units", () => {
const left = { ...dimensionless, x: 2, y: 2 };
const right = { ...dimensionless, x: 2, y: -1, z: 2 };
expect(unitSystem.divide(left, right)).toEqual({ ...dimensionless, y: 3, z: -2 });
});
});
});
});
70 changes: 0 additions & 70 deletions src/measure/__test__/unitValueArithmeticTests.ts

This file was deleted.

12 changes: 7 additions & 5 deletions src/measure/exponentTypeArithmetic.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export type Negative<N> = N extends number
? `${N}` extends `-${infer Pos extends number}`
? Pos
: `-${N}` extends `${infer Neg extends number}`
? Neg
: 0
? 0 extends N
? 0
: `${N}` extends `-${infer Pos extends number}`
? Pos
: `-${N}` extends `${infer Neg extends number}`
? Neg
: 0
: never;

export type AddIntegers<
Expand Down
35 changes: 20 additions & 15 deletions src/measure/format.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { SymbolAndExponent, UnitWithSymbols } from "./unitTypeArithmetic";

export function defaultFormatUnit(unit: UnitWithSymbols): string {
const dimensions = Object.keys(unit)
.map(dimension => unit[dimension])
.filter(isDimensionPresent)
.sort(orderDimensions);

if (dimensions.length === 0) {
import { UnitSystem } from "./unitSystem";
import { Unit } from "./unitTypeArithmetic";

type SymbolAndExponent = [symbol: string, exponent: number];

export function defaultFormatUnit<Basis>(unit: Unit<Basis>, unitSystem: UnitSystem<Basis>): string {
const positive: SymbolAndExponent[] = [];
const negative: SymbolAndExponent[] = [];
unitSystem.getDimensions().forEach(dimension => {
const exponent = unit[dimension];
if (exponent < 0) {
negative.push([unitSystem.getSymbol(dimension), unit[dimension]]);
} else if (exponent > 0) {
positive.push([unitSystem.getSymbol(dimension), unit[dimension]]);
}
});

if (positive.length === 0 && negative.length === 0) {
return "";
}

const positive = dimensions.filter(([_, dim]) => dim > 0);
const negative = dimensions.filter(([_, dim]) => dim < 0);
positive.sort(orderDimensions);
negative.sort(orderDimensions);

if (positive.length === 0) {
return formatDimensions(negative);
Expand All @@ -26,10 +35,6 @@ export function defaultFormatUnit(unit: UnitWithSymbols): string {
return `${numerator} / ${maybeParenthesize(denominator, negative.length !== 1)}`;
}

function isDimensionPresent(dimension: SymbolAndExponent | undefined): dimension is SymbolAndExponent {
return dimension !== undefined && dimension[1] !== 0;
}

function orderDimensions([leftSymbol]: SymbolAndExponent, [rightSymbol]: SymbolAndExponent): number {
return leftSymbol < rightSymbol ? -1 : 1;
}
Expand Down
Loading

0 comments on commit de687d4

Please sign in to comment.