measurable 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +179 -35
  3. package/dist/dimensions/angle.js +2 -2
  4. package/dist/dimensions/area.d.ts +14 -0
  5. package/dist/dimensions/area.js +25 -0
  6. package/dist/dimensions/data.d.ts +10 -0
  7. package/dist/dimensions/data.js +36 -0
  8. package/dist/dimensions/energy.d.ts +14 -0
  9. package/dist/dimensions/energy.js +22 -0
  10. package/dist/dimensions/force.js +2 -2
  11. package/dist/dimensions/frequency.d.ts +7 -0
  12. package/dist/dimensions/frequency.js +11 -0
  13. package/dist/dimensions/illuminance.d.ts +9 -0
  14. package/dist/dimensions/illuminance.js +13 -0
  15. package/dist/dimensions/index.d.ts +9 -0
  16. package/dist/dimensions/index.js +9 -0
  17. package/dist/dimensions/length.js +2 -2
  18. package/dist/dimensions/luminance.d.ts +7 -0
  19. package/dist/dimensions/luminance.js +16 -0
  20. package/dist/dimensions/luminousIntensity.d.ts +9 -0
  21. package/dist/dimensions/luminousIntensity.js +16 -0
  22. package/dist/dimensions/mass.js +2 -2
  23. package/dist/dimensions/power.d.ts +9 -0
  24. package/dist/dimensions/power.js +13 -0
  25. package/dist/dimensions/pressure.d.ts +14 -0
  26. package/dist/dimensions/pressure.js +18 -0
  27. package/dist/dimensions/temperature.js +7 -1
  28. package/dist/dimensions/time.js +2 -2
  29. package/dist/dimensions/volume.js +2 -2
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +2 -1
  32. package/dist/lib/Dimension.d.ts +37 -11
  33. package/dist/lib/Dimension.js +49 -14
  34. package/dist/lib/MeasurementSystem.js +2 -2
  35. package/dist/lib/Quantity.d.ts +24 -10
  36. package/dist/lib/Quantity.js +39 -37
  37. package/dist/lib/Rational.d.ts +58 -0
  38. package/dist/lib/Rational.js +174 -0
  39. package/dist/lib/Unit.d.ts +41 -7
  40. package/dist/lib/Unit.js +35 -6
  41. package/dist/lib/prefixes.d.ts +2 -2
  42. package/dist/lib/prefixes.js +1 -1
  43. package/dist/systems/imperial.js +19 -1
  44. package/dist/systems/metric.js +25 -1
  45. package/dist/systems/usCustomary.js +19 -1
  46. package/dist/utils/definePrefixed.d.ts +35 -0
  47. package/dist/utils/definePrefixed.js +61 -0
  48. package/dist/utils/scaleOf.d.ts +3 -0
  49. package/dist/utils/scaleOf.js +6 -0
  50. package/package.json +8 -3
@@ -3,20 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Quantity = void 0;
4
4
  const AmbiguousUnitError_1 = require("../errors/AmbiguousUnitError");
5
5
  const UnknownUnitError_1 = require("../errors/UnknownUnitError");
6
- const scale_1 = require("./scale");
6
+ const scaleOf_1 = require("../utils/scaleOf");
7
+ const Rational_1 = require("./Rational");
7
8
  /** A magnitude paired with a unit (e.g. `5` `kilometer`). */
8
9
  class Quantity {
9
10
  constructor(magnitude, unit) {
10
- this.magnitude = magnitude;
11
11
  this.unit = unit;
12
+ this.rational = Rational_1.Rational.from(magnitude);
13
+ }
14
+ /** The magnitude as a `number`, derived from {@link rational}. */
15
+ get magnitude() {
16
+ return this.rational.toNumber();
12
17
  }
13
18
  /** Return an equivalent quantity expressed in `target`. */
14
19
  to(target) {
15
- return new Quantity(this.in(target), target);
20
+ return new Quantity(this.inRational(target), target);
16
21
  }
17
22
  /** Return this quantity's raw magnitude expressed in `target`. */
18
23
  in(target) {
19
- return this.unit.dimension.convert(this.magnitude, this.unit, target);
24
+ return this.inRational(target).toNumber();
25
+ }
26
+ /** This quantity's exact magnitude expressed in `target`. */
27
+ inRational(target) {
28
+ return this.unit.dimension.convertRational(this.rational, this.unit, target);
20
29
  }
21
30
  /** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
22
31
  toString() {
@@ -33,19 +42,19 @@ class Quantity {
33
42
  * difference.
34
43
  */
35
44
  plus(other) {
36
- return new Quantity(this.magnitude + other.in(this.unit), this.unit);
45
+ return new Quantity(this.rational.plus(other.inRational(this.unit)), this.unit);
37
46
  }
38
47
  /** Subtract another quantity, returned in this quantity's unit. */
39
48
  minus(other) {
40
- return new Quantity(this.magnitude - other.in(this.unit), this.unit);
49
+ return new Quantity(this.rational.minus(other.inRational(this.unit)), this.unit);
41
50
  }
42
51
  /** Scale this quantity by a dimensionless factor. */
43
52
  times(factor) {
44
- return new Quantity(this.magnitude * factor, this.unit);
53
+ return new Quantity(this.rational.times(Rational_1.Rational.from(factor)), this.unit);
45
54
  }
46
55
  /** Divide this quantity by a dimensionless divisor. */
47
56
  dividedBy(divisor) {
48
- return new Quantity(this.magnitude / divisor, this.unit);
57
+ return new Quantity(this.rational.dividedBy(Rational_1.Rational.from(divisor)), this.unit);
49
58
  }
50
59
  /**
51
60
  * Divide this quantity by `other` of the same dimension, yielding the
@@ -54,15 +63,15 @@ class Quantity {
54
63
  * Throws {@link InvalidConversionError} across dimensions.
55
64
  */
56
65
  ratioTo(other) {
57
- return this.magnitude / other.in(this.unit);
66
+ return this.rational.dividedBy(other.inRational(this.unit)).toNumber();
58
67
  }
59
68
  /** Return this quantity with its magnitude negated. */
60
69
  negate() {
61
- return new Quantity(-this.magnitude, this.unit);
70
+ return new Quantity(this.rational.negate(), this.unit);
62
71
  }
63
72
  /** Return this quantity with a non-negative magnitude. */
64
73
  abs() {
65
- return new Quantity(Math.abs(this.magnitude), this.unit);
74
+ return new Quantity(this.rational.abs(), this.unit);
66
75
  }
67
76
  /** Clamp this quantity to the range [`lower`, `upper`], returned in this unit. */
68
77
  clamp(lower, upper) {
@@ -98,11 +107,12 @@ class Quantity {
98
107
  /**
99
108
  * Whether this quantity equals `other`, compared in this quantity's unit.
100
109
  * Throws {@link InvalidConversionError} if the operands belong to different
101
- * dimensions. Comparison is exact, so values that differ only by
102
- * floating-point rounding from a conversion may compare unequal.
110
+ * dimensions. Comparison is exact rational equality, so quantities that are
111
+ * mathematically equal compare equal even if reaching them involved a
112
+ * conversion that would have drifted in floating point.
103
113
  */
104
114
  equals(other) {
105
- return this.magnitude === other.in(this.unit);
115
+ return this.rational.equals(other.inRational(this.unit));
106
116
  }
107
117
  /** Whether this quantity does not equal `other`. */
108
118
  notEquals(other) {
@@ -110,19 +120,19 @@ class Quantity {
110
120
  }
111
121
  /** Whether this quantity is less than `other`. */
112
122
  lessThan(other) {
113
- return this.magnitude < other.in(this.unit);
123
+ return this.rational.compare(other.inRational(this.unit)) < 0;
114
124
  }
115
125
  /** Whether this quantity is greater than `other`. */
116
126
  greaterThan(other) {
117
- return this.magnitude > other.in(this.unit);
127
+ return this.rational.compare(other.inRational(this.unit)) > 0;
118
128
  }
119
129
  /** Whether this quantity is less than or equal to `other`. */
120
130
  lessThanOrEqual(other) {
121
- return this.magnitude <= other.in(this.unit);
131
+ return this.rational.compare(other.inRational(this.unit)) <= 0;
122
132
  }
123
133
  /** Whether this quantity is greater than or equal to `other`. */
124
134
  greaterThanOrEqual(other) {
125
- return this.magnitude >= other.in(this.unit);
135
+ return this.rational.compare(other.inRational(this.unit)) >= 0;
126
136
  }
127
137
  /** Alias for {@link equals}. */
128
138
  eq(other) {
@@ -153,26 +163,19 @@ class Quantity {
153
163
  * if larger, `0` if equal. Suitable as an `Array#sort` comparator.
154
164
  */
155
165
  compareTo(other) {
156
- const value = other.in(this.unit);
157
- if (this.magnitude < value) {
158
- return -1;
159
- }
160
- if (this.magnitude > value) {
161
- return 1;
162
- }
163
- return 0;
166
+ return this.rational.compare(other.inRational(this.unit));
164
167
  }
165
168
  /** Whether this quantity's magnitude is exactly zero. */
166
169
  isZero() {
167
- return this.magnitude === 0;
170
+ return this.rational.sign() === 0;
168
171
  }
169
172
  /** Whether this quantity's magnitude is greater than zero. */
170
173
  isPositive() {
171
- return this.magnitude > 0;
174
+ return this.rational.sign() > 0;
172
175
  }
173
176
  /** Whether this quantity's magnitude is less than zero. */
174
177
  isNegative() {
175
- return this.magnitude < 0;
178
+ return this.rational.sign() < 0;
176
179
  }
177
180
  /**
178
181
  * Parse a string into a `Quantity` using a dimension's known units and aliases.
@@ -181,7 +184,7 @@ class Quantity {
181
184
  * - `"5 hr"` -> `Quantity(5, hour)`
182
185
  * - `"5hr 20min"` -> `Quantity(320, minute)`
183
186
  *
184
- * Compound inputs are summed in base units and returned in the *finest*
187
+ * Compound inputs are summed (exactly) and returned in the *finest*
185
188
  * (smallest-scale) unit present, so `"5hr 20min"` collapses to `320 minute`.
186
189
  *
187
190
  * When a token is a shared alias (e.g. `"ton"` → short ton & long ton), pass
@@ -189,22 +192,21 @@ class Quantity {
189
192
  */
190
193
  static parse(str, dimension, options = {}) {
191
194
  const pattern = /(-?\d+(?:\.\d+)?)\s*([^\d\s]+)/g;
192
- let total = 0; // accumulated in base units
195
+ let total;
193
196
  let finest;
194
- let count = 0;
195
197
  for (let match = pattern.exec(str); match !== null; match = pattern.exec(str)) {
196
198
  const value = Number.parseFloat(match[1]);
197
199
  const unit = resolve(match[2], dimension, options.prefer);
198
- total += unit.toBase(value);
199
- if (!finest || (0, scale_1.scaleOf)(unit) < (0, scale_1.scaleOf)(finest)) {
200
+ const quantity = new Quantity(value, unit);
201
+ total = total ? total.plus(quantity) : quantity;
202
+ if (!finest || (0, scaleOf_1.scaleOf)(unit) < (0, scaleOf_1.scaleOf)(finest)) {
200
203
  finest = unit;
201
204
  }
202
- count += 1;
203
205
  }
204
- if (count === 0 || !finest) {
206
+ if (!total || !finest) {
205
207
  throw new Error(`Could not parse a quantity from "${str}"`);
206
208
  }
207
- return new Quantity(finest.fromBase(total), finest);
209
+ return total.to(finest);
208
210
  }
209
211
  /** The smallest of the given quantities (by value); requires at least one. */
210
212
  static min(first, ...rest) {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * An exact rational number (`n / d`) used to make conversions between linear
3
+ * and affine units lossless. Such conversions are inherently rational — a foot
4
+ * is exactly `3048/10000` m and an inch exactly `254/10000` m, so foot → inch is
5
+ * exactly `12` — but baking those ratios into binary `number` scales rounds the
6
+ * result (`12.000000000000002`). Doing the arithmetic over integer numerator /
7
+ * denominator pairs and collapsing to a float only at the end avoids the drift.
8
+ *
9
+ * Instances are immutable and always stored in lowest terms with a positive
10
+ * denominator. `bigint` is used so cross-multiplying large scales (e.g. a
11
+ * mile's `1609344`) cannot overflow.
12
+ */
13
+ export declare class Rational {
14
+ readonly n: bigint;
15
+ readonly d: bigint;
16
+ /**
17
+ * Build a rational from integer numerator and denominator. Pass exact ratios
18
+ * the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
19
+ * For a decimal value, use {@link Rational.fromNumber} instead.
20
+ */
21
+ constructor(numerator: bigint | number, denominator?: bigint | number);
22
+ /** Coerce a `number | Rational` to a `Rational`, parsing numbers as decimals. */
23
+ static from(value: number | Rational): Rational;
24
+ /**
25
+ * Convert a `number` to the exact rational the author *wrote*, by reading its
26
+ * shortest round-tripping decimal (`(0.0254).toString() === "0.0254"` →
27
+ * `254/10000`). This recovers the intended terminating decimal. A value that
28
+ * was never terminating in source (e.g. Fahrenheit's `5 / 9`) is captured as
29
+ * the exact rational of its nearest double — no worse than a raw float, and
30
+ * the conversion is still done in one rounding instead of several. To avoid
31
+ * that, pass an exact {@link Rational} (e.g. `new Rational(5, 9)`) instead.
32
+ */
33
+ private static fromNumber;
34
+ plus(other: Rational): Rational;
35
+ minus(other: Rational): Rational;
36
+ times(other: Rational): Rational;
37
+ dividedBy(other: Rational): Rational;
38
+ negate(): Rational;
39
+ abs(): Rational;
40
+ /** Whether this equals `other` exactly (both are stored in canonical form). */
41
+ equals(other: Rational): boolean;
42
+ /** Alias for {@link plus}. */
43
+ add(other: Rational): Rational;
44
+ /** Alias for {@link minus}. */
45
+ sub(other: Rational): Rational;
46
+ /** Alias for {@link times}. */
47
+ mul(other: Rational): Rational;
48
+ /** Alias for {@link dividedBy}. */
49
+ div(other: Rational): Rational;
50
+ /** Alias for {@link equals}. */
51
+ eq(other: Rational): boolean;
52
+ /** `-1` if this is smaller than `other`, `1` if larger, `0` if equal. */
53
+ compare(other: Rational): number;
54
+ /** Sign of the value: `-1`, `0`, or `1`. */
55
+ sign(): number;
56
+ /** Collapse to the nearest `number`. */
57
+ toNumber(): number;
58
+ }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Rational = void 0;
4
+ /**
5
+ * An exact rational number (`n / d`) used to make conversions between linear
6
+ * and affine units lossless. Such conversions are inherently rational — a foot
7
+ * is exactly `3048/10000` m and an inch exactly `254/10000` m, so foot → inch is
8
+ * exactly `12` — but baking those ratios into binary `number` scales rounds the
9
+ * result (`12.000000000000002`). Doing the arithmetic over integer numerator /
10
+ * denominator pairs and collapsing to a float only at the end avoids the drift.
11
+ *
12
+ * Instances are immutable and always stored in lowest terms with a positive
13
+ * denominator. `bigint` is used so cross-multiplying large scales (e.g. a
14
+ * mile's `1609344`) cannot overflow.
15
+ */
16
+ class Rational {
17
+ /**
18
+ * Build a rational from integer numerator and denominator. Pass exact ratios
19
+ * the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
20
+ * For a decimal value, use {@link Rational.fromNumber} instead.
21
+ */
22
+ constructor(numerator, denominator = 1n) {
23
+ let n = toBigInt(numerator);
24
+ let d = toBigInt(denominator);
25
+ if (d === 0n) {
26
+ throw new Error("Rational denominator cannot be zero");
27
+ }
28
+ if (d < 0n) {
29
+ n = -n;
30
+ d = -d;
31
+ }
32
+ const g = gcd(n, d) || 1n;
33
+ this.n = n / g;
34
+ this.d = d / g;
35
+ }
36
+ /** Coerce a `number | Rational` to a `Rational`, parsing numbers as decimals. */
37
+ static from(value) {
38
+ return value instanceof Rational ? new this(value.n, value.d) : Rational.fromNumber(value);
39
+ }
40
+ /**
41
+ * Convert a `number` to the exact rational the author *wrote*, by reading its
42
+ * shortest round-tripping decimal (`(0.0254).toString() === "0.0254"` →
43
+ * `254/10000`). This recovers the intended terminating decimal. A value that
44
+ * was never terminating in source (e.g. Fahrenheit's `5 / 9`) is captured as
45
+ * the exact rational of its nearest double — no worse than a raw float, and
46
+ * the conversion is still done in one rounding instead of several. To avoid
47
+ * that, pass an exact {@link Rational} (e.g. `new Rational(5, 9)`) instead.
48
+ */
49
+ static fromNumber(value) {
50
+ if (!Number.isFinite(value)) {
51
+ throw new Error(`Cannot derive a rational from ${value}`);
52
+ }
53
+ const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(value.toString());
54
+ if (!match) {
55
+ throw new Error(`Cannot derive a rational from ${value}`);
56
+ }
57
+ const [, sign, intPart, fracPart = "", expPart] = match;
58
+ let n = BigInt(intPart + fracPart);
59
+ let d = 10n ** BigInt(fracPart.length);
60
+ const exp = expPart ? Number(expPart) : 0;
61
+ if (exp > 0) {
62
+ n *= 10n ** BigInt(exp);
63
+ }
64
+ else if (exp < 0) {
65
+ d *= 10n ** BigInt(-exp);
66
+ }
67
+ return new Rational(sign === "-" ? -n : n, d);
68
+ }
69
+ plus(other) {
70
+ return new Rational(this.n * other.d + other.n * this.d, this.d * other.d);
71
+ }
72
+ minus(other) {
73
+ return new Rational(this.n * other.d - other.n * this.d, this.d * other.d);
74
+ }
75
+ times(other) {
76
+ return new Rational(this.n * other.n, this.d * other.d);
77
+ }
78
+ dividedBy(other) {
79
+ return new Rational(this.n * other.d, this.d * other.n);
80
+ }
81
+ negate() {
82
+ return new Rational(-this.n, this.d);
83
+ }
84
+ abs() {
85
+ return new Rational(this.n < 0n ? -this.n : this.n, this.d);
86
+ }
87
+ /** Whether this equals `other` exactly (both are stored in canonical form). */
88
+ equals(other) {
89
+ return this.n === other.n && this.d === other.d;
90
+ }
91
+ /** Alias for {@link plus}. */
92
+ add(other) {
93
+ return this.plus(other);
94
+ }
95
+ /** Alias for {@link minus}. */
96
+ sub(other) {
97
+ return this.minus(other);
98
+ }
99
+ /** Alias for {@link times}. */
100
+ mul(other) {
101
+ return this.times(other);
102
+ }
103
+ /** Alias for {@link dividedBy}. */
104
+ div(other) {
105
+ return this.dividedBy(other);
106
+ }
107
+ /** Alias for {@link equals}. */
108
+ eq(other) {
109
+ return this.equals(other);
110
+ }
111
+ /** `-1` if this is smaller than `other`, `1` if larger, `0` if equal. */
112
+ compare(other) {
113
+ // Denominators are always positive, so cross-multiplication preserves order.
114
+ const lhs = this.n * other.d;
115
+ const rhs = other.n * this.d;
116
+ return lhs < rhs ? -1 : lhs > rhs ? 1 : 0;
117
+ }
118
+ /** Sign of the value: `-1`, `0`, or `1`. */
119
+ sign() {
120
+ return this.n < 0n ? -1 : this.n > 0n ? 1 : 0;
121
+ }
122
+ /** Collapse to the nearest `number`. */
123
+ toNumber() {
124
+ const { n, d } = this;
125
+ if (n === 0n) {
126
+ return 0;
127
+ }
128
+ // Fast path: both operands are exactly representable as doubles, so a single
129
+ // IEEE division is correctly rounded.
130
+ if (n >= -MAX_EXACT_INT && n <= MAX_EXACT_INT && d <= MAX_EXACT_INT) {
131
+ return Number(n) / Number(d);
132
+ }
133
+ // Otherwise converting each operand to a double first would round it before
134
+ // dividing and lose precision (e.g. denominators like 10^24). Long-divide to
135
+ // SIG significant digits and let Number()'s correctly-rounded decimal parser
136
+ // produce the nearest double, independent of magnitude.
137
+ const SIG = 20;
138
+ const negative = n < 0n;
139
+ const num = negative ? -n : n;
140
+ // floor(log10(num / d)), accurate to within 1 — enough to capture SIG digits.
141
+ const exponent = num.toString().length - d.toString().length;
142
+ const shift = SIG - 1 - exponent;
143
+ const scaledNum = shift >= 0 ? num * 10n ** BigInt(shift) : num;
144
+ const scaledDen = shift >= 0 ? d : d * 10n ** BigInt(-shift);
145
+ let digits = scaledNum / scaledDen;
146
+ if (2n * (scaledNum % scaledDen) >= scaledDen) {
147
+ digits += 1n; // round half up on the last significant digit
148
+ }
149
+ return Number(`${negative ? "-" : ""}${digits}e${exponent - (SIG - 1)}`);
150
+ }
151
+ }
152
+ exports.Rational = Rational;
153
+ /**
154
+ * One past `Number.MAX_SAFE_INTEGER` (i.e. 2^53): every integer with magnitude
155
+ * at most this is exactly representable as a double, so `Number(n)` is lossless.
156
+ */
157
+ const MAX_EXACT_INT = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
158
+ function gcd(a, b) {
159
+ let x = a < 0n ? -a : a;
160
+ let y = b < 0n ? -b : b;
161
+ while (y) {
162
+ [x, y] = [y, x % y];
163
+ }
164
+ return x;
165
+ }
166
+ function toBigInt(value) {
167
+ if (typeof value === "bigint") {
168
+ return value;
169
+ }
170
+ if (!Number.isInteger(value)) {
171
+ throw new Error(`Rational numerator and denominator must be integers; got ${value}`);
172
+ }
173
+ return BigInt(value);
174
+ }
@@ -1,19 +1,44 @@
1
1
  import type { Dimension } from "./Dimension";
2
- interface UnitOptions {
2
+ import type { Rational } from "./Rational";
3
+ /**
4
+ * An affine transform to the base unit, `base = value * scale + offset`, with
5
+ * both terms held as exact rationals. Covers the base unit (`1·x + 0`), plain
6
+ * linear units (`scale·x + 0`), and offset units like Celsius (`1·x + 273.15`).
7
+ */
8
+ export interface LinearTransform {
9
+ scale: Rational;
10
+ offset: Rational;
11
+ }
12
+ interface BaseUnitOptions {
3
13
  name: string;
4
14
  dimension: Dimension;
15
+ }
16
+ interface LinearConversionOptions {
17
+ /** Exact transform for linear / affine units (the common case). */
18
+ linear: LinearTransform;
19
+ }
20
+ interface CustomConversionOptions {
21
+ /** Hand-written transform for non-linear units; mutually exclusive with `linear`. */
5
22
  toBase: (value: number) => number;
6
23
  fromBase: (value: number) => number;
7
24
  }
25
+ export type UnitConversionOptions = LinearConversionOptions | CustomConversionOptions;
26
+ type UnitOptions = BaseUnitOptions & UnitConversionOptions;
8
27
  /**
9
28
  * A single unit of measurement (e.g. "meter", "celsius").
10
29
  *
11
30
  * A `Unit` is a passive handle: it knows its name, its home {@link Dimension},
12
31
  * and how to transform a value to and from that dimension's canonical base
13
32
  * unit. It does NOT know about other units or store pairwise conversions — all
14
- * conversion math lives in {@link Dimension}, derived from these two transforms.
15
- * Because the reverse direction (`fromBase`) is the mathematical inverse of the
16
- * forward direction (`toBase`), the two can never fall out of sync.
33
+ * conversion math lives in {@link Dimension}, derived from this transform.
34
+ *
35
+ * Almost every unit relates to the base by an affine map (`value * scale +
36
+ * offset`), so the transform is normally a {@link LinearTransform} of exact
37
+ * rationals — a single source of truth from which {@link toBase} / {@link
38
+ * fromBase} are derived, and which lets {@link Dimension.convert} stay exact.
39
+ * Only genuinely non-linear units (e.g. logarithmic scales, defined via
40
+ * `Dimension.custom`) fall back to a hand-written `toBase` / `fromBase` pair,
41
+ * since no `scale`/`offset` can express a curve like `10^x`.
17
42
  *
18
43
  * A unit belongs to exactly one dimension, but may belong to many
19
44
  * {@link MeasurementSystem}s (metric/imperial/…); that membership lives on the
@@ -28,8 +53,17 @@ interface UnitOptions {
28
53
  export declare class Unit {
29
54
  readonly name: string;
30
55
  readonly dimension: Dimension;
31
- readonly toBase: (value: number) => number;
32
- readonly fromBase: (value: number) => number;
33
- constructor({ name, dimension, toBase, fromBase }: UnitOptions);
56
+ private readonly conversion;
57
+ constructor({ name, dimension, ...conversionOptions }: UnitOptions);
58
+ /**
59
+ * Exact affine transform to the base unit, present for all but non-linear
60
+ * units. When both ends of a conversion have one, {@link Dimension.convert}
61
+ * routes through exact rational arithmetic instead of lossy float scaling.
62
+ */
63
+ get linear(): LinearTransform | undefined;
64
+ /** Convert a value in this unit to the dimension's base unit. */
65
+ toBase(value: number): number;
66
+ /** Convert a value in the dimension's base unit to this unit. */
67
+ fromBase(value: number): number;
34
68
  }
35
69
  export {};
package/dist/lib/Unit.js CHANGED
@@ -7,9 +7,15 @@ exports.Unit = void 0;
7
7
  * A `Unit` is a passive handle: it knows its name, its home {@link Dimension},
8
8
  * and how to transform a value to and from that dimension's canonical base
9
9
  * unit. It does NOT know about other units or store pairwise conversions — all
10
- * conversion math lives in {@link Dimension}, derived from these two transforms.
11
- * Because the reverse direction (`fromBase`) is the mathematical inverse of the
12
- * forward direction (`toBase`), the two can never fall out of sync.
10
+ * conversion math lives in {@link Dimension}, derived from this transform.
11
+ *
12
+ * Almost every unit relates to the base by an affine map (`value * scale +
13
+ * offset`), so the transform is normally a {@link LinearTransform} of exact
14
+ * rationals — a single source of truth from which {@link toBase} / {@link
15
+ * fromBase} are derived, and which lets {@link Dimension.convert} stay exact.
16
+ * Only genuinely non-linear units (e.g. logarithmic scales, defined via
17
+ * `Dimension.custom`) fall back to a hand-written `toBase` / `fromBase` pair,
18
+ * since no `scale`/`offset` can express a curve like `10^x`.
13
19
  *
14
20
  * A unit belongs to exactly one dimension, but may belong to many
15
21
  * {@link MeasurementSystem}s (metric/imperial/…); that membership lives on the
@@ -22,11 +28,34 @@ exports.Unit = void 0;
22
28
  * (`base`, `unit`, `affine`, `custom`) rather than constructed directly.
23
29
  */
24
30
  class Unit {
25
- constructor({ name, dimension, toBase, fromBase }) {
31
+ constructor({ name, dimension, ...conversionOptions }) {
26
32
  this.name = name;
27
33
  this.dimension = dimension;
28
- this.toBase = toBase;
29
- this.fromBase = fromBase;
34
+ this.conversion = conversionOptions;
35
+ }
36
+ /**
37
+ * Exact affine transform to the base unit, present for all but non-linear
38
+ * units. When both ends of a conversion have one, {@link Dimension.convert}
39
+ * routes through exact rational arithmetic instead of lossy float scaling.
40
+ */
41
+ get linear() {
42
+ return "linear" in this.conversion ? this.conversion.linear : undefined;
43
+ }
44
+ /** Convert a value in this unit to the dimension's base unit. */
45
+ toBase(value) {
46
+ if ("linear" in this.conversion) {
47
+ const linear = this.conversion.linear;
48
+ return value * linear.scale.toNumber() + linear.offset.toNumber();
49
+ }
50
+ return this.conversion.toBase(value);
51
+ }
52
+ /** Convert a value in the dimension's base unit to this unit. */
53
+ fromBase(value) {
54
+ if ("linear" in this.conversion) {
55
+ const linear = this.conversion.linear;
56
+ return (value - linear.offset.toNumber()) / linear.scale.toNumber();
57
+ }
58
+ return this.conversion.fromBase(value);
30
59
  }
31
60
  }
32
61
  exports.Unit = Unit;
@@ -16,8 +16,8 @@ export interface PrefixReference {
16
16
  name: string;
17
17
  /** Primary symbol, e.g. "m" → "km", "mm", … */
18
18
  symbol: string;
19
- /** Scale of the reference relative to its dimension's base (meter → 1, gram → 0.001). */
20
- scale: number;
19
+ /** Scale of the reference relative to its dimension's base (meter → 1, gram → 0.001) (default 1). */
20
+ scale?: number;
21
21
  }
22
22
  /**
23
23
  * Define metric-prefixed variants of a reference unit on a dimension. Each
@@ -50,7 +50,7 @@ function definePrefixed(dimension, reference, prefixes = exports.SI_PREFIXES) {
50
50
  if (prefix.name === "micro") {
51
51
  aliases.push(`u${reference.symbol}`);
52
52
  }
53
- units[name] = dimension.unit(name, reference.scale * prefix.factor, aliases);
53
+ units[name] = dimension.unit(name, (reference.scale ?? 1) * prefix.factor, aliases);
54
54
  }
55
55
  return units;
56
56
  }
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.imperial = void 0;
4
+ const area_1 = require("../dimensions/area");
5
+ const energy_1 = require("../dimensions/energy");
6
+ const illuminance_1 = require("../dimensions/illuminance");
4
7
  const length_1 = require("../dimensions/length");
8
+ const luminousIntensity_1 = require("../dimensions/luminousIntensity");
5
9
  const mass_1 = require("../dimensions/mass");
10
+ const power_1 = require("../dimensions/power");
11
+ const pressure_1 = require("../dimensions/pressure");
6
12
  const temperature_1 = require("../dimensions/temperature");
7
13
  const volume_1 = require("../dimensions/volume");
8
14
  const MeasurementSystem_1 = require("../lib/MeasurementSystem");
@@ -10,9 +16,21 @@ const MeasurementSystem_1 = require("../lib/MeasurementSystem");
10
16
  exports.imperial = new MeasurementSystem_1.MeasurementSystem("imperial").add(
11
17
  // length
12
18
  length_1.inch, length_1.foot, length_1.yard, length_1.mile,
19
+ // area
20
+ area_1.squareInch, area_1.squareFoot, area_1.squareYard, area_1.acre, area_1.squareMile,
13
21
  // mass
14
22
  mass_1.pound, mass_1.ounce, mass_1.stone, mass_1.longTon,
15
23
  // volume
16
24
  volume_1.imperialGallon, volume_1.imperialQuart, volume_1.imperialPint, volume_1.imperialGill, volume_1.imperialFluidOunce,
17
25
  // temperature
18
- temperature_1.fahrenheit);
26
+ temperature_1.fahrenheit,
27
+ // energy
28
+ energy_1.britishThermalUnit,
29
+ // power
30
+ power_1.horsepower,
31
+ // pressure
32
+ pressure_1.psi, pressure_1.inchOfMercury, pressure_1.inchOfWater,
33
+ // illuminance
34
+ illuminance_1.footCandle,
35
+ // luminous intensity
36
+ luminousIntensity_1.candlepower);
@@ -1,8 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.metric = void 0;
4
+ const area_1 = require("../dimensions/area");
5
+ const energy_1 = require("../dimensions/energy");
6
+ const frequency_1 = require("../dimensions/frequency");
7
+ const illuminance_1 = require("../dimensions/illuminance");
4
8
  const length_1 = require("../dimensions/length");
9
+ const luminance_1 = require("../dimensions/luminance");
10
+ const luminousIntensity_1 = require("../dimensions/luminousIntensity");
5
11
  const mass_1 = require("../dimensions/mass");
12
+ const power_1 = require("../dimensions/power");
13
+ const pressure_1 = require("../dimensions/pressure");
6
14
  const temperature_1 = require("../dimensions/temperature");
7
15
  const volume_1 = require("../dimensions/volume");
8
16
  const MeasurementSystem_1 = require("../lib/MeasurementSystem");
@@ -10,9 +18,25 @@ const MeasurementSystem_1 = require("../lib/MeasurementSystem");
10
18
  exports.metric = new MeasurementSystem_1.MeasurementSystem("metric").add(
11
19
  // length
12
20
  length_1.meter, ...Object.values(length_1.metricLength),
21
+ // area
22
+ area_1.squareMeter, area_1.squareKilometer, area_1.hectare, area_1.are, area_1.squareCentimeter, area_1.squareMillimeter,
13
23
  // mass
14
24
  mass_1.gram, mass_1.tonne, ...Object.values(mass_1.metricMass),
15
25
  // volume
16
26
  volume_1.liter, ...Object.values(volume_1.metricVolume),
17
27
  // temperature
18
- temperature_1.kelvin, temperature_1.celsius);
28
+ temperature_1.kelvin, temperature_1.celsius,
29
+ // energy
30
+ energy_1.joule, ...Object.values(energy_1.metricEnergy), energy_1.wattHour, ...Object.values(energy_1.metricWattHour), energy_1.calorie, energy_1.kilocalorie,
31
+ // power
32
+ power_1.watt, ...Object.values(power_1.metricPower), power_1.metricHorsepower,
33
+ // pressure
34
+ pressure_1.pascal, ...Object.values(pressure_1.metricPressure), pressure_1.bar, pressure_1.millibar,
35
+ // frequency
36
+ frequency_1.hertz, ...Object.values(frequency_1.metricFrequency),
37
+ // illuminance
38
+ illuminance_1.lux, ...Object.values(illuminance_1.metricIlluminance), illuminance_1.phot,
39
+ // luminance
40
+ luminance_1.candelaPerSquareMeter, luminance_1.stilb,
41
+ // luminous intensity
42
+ luminousIntensity_1.candela, ...Object.values(luminousIntensity_1.metricLuminousIntensity));