measurable 3.0.0 → 3.1.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.1.0] - 2026-06-19
9
+
10
+ This release adds a best-fit primitive and gives every thrown error a dedicated,
11
+ catchable class. All changes are backward-compatible.
12
+
13
+ ### Added
14
+
15
+ - **`Quantity.best(...units)`** — re-expresses a quantity in the best-fit unit
16
+ among the given ones: the largest unit whose absolute magnitude is still ≥ 1
17
+ (falling back to the smallest). Candidate order doesn't matter; each must
18
+ belong to the quantity's dimension.
19
+ - **Dedicated error classes**, all extending `Error` and exported from
20
+ `measurable`, so failures can be branched on with `instanceof`:
21
+ `ArgumentError` (invalid argument — e.g. `best()` with no units, or a
22
+ non-integer / zero-denominator / non-finite `Rational`), `ParseError` (a string
23
+ could not be parsed into a quantity), `DuplicateUnitError` (a unit name already
24
+ exists in its dimension), and `UnsupportedDimensionError` (a measurement system
25
+ has no units of a dimension to `express` in).
26
+
27
+ ### Changed
28
+
29
+ - **`MeasurementSystem.express` now delegates to `Quantity.best`** — same
30
+ behavior, with the best-fit logic living in one place.
31
+ - **Every previously bare `throw new Error` now throws a dedicated class.**
32
+ Messages are unchanged and all classes extend `Error`, so existing `catch`
33
+ blocks keep working.
34
+
35
+ ### Deprecated
36
+
37
+ - **`InvalidConversionError` is renamed to `DimensionMismatchError`** — it is
38
+ thrown for any cross-dimension operation (converting, comparing, or combining
39
+ units), not just conversions. The old name remains exported as a deprecated
40
+ alias of the same class and still works with `instanceof`.
41
+
8
42
  ## [3.0.0] - 2026-06-18
9
43
 
10
44
  This release adds value **formatting**. Units now carry a `symbol` and `plural`,
@@ -142,6 +176,7 @@ to a `number` at the edge, so `foot → inch` is exactly `12` (not
142
176
  `MeasurementSystem`), string parsing via `Quantity.parse`, and the first set
143
177
  of built-in dimensions and measurement systems.
144
178
 
179
+ [3.1.0]: https://github.com/mhuggins/measurable/compare/v3.0.0...v3.1.0
145
180
  [3.0.0]: https://github.com/mhuggins/measurable/compare/v2.0.0...v3.0.0
146
181
  [2.0.0]: https://github.com/mhuggins/measurable/compare/v1.1.1...v2.0.0
147
182
  [1.1.1]: https://github.com/mhuggins/measurable/compare/v1.1.0...v1.1.1
package/README.md CHANGED
@@ -206,6 +206,25 @@ metric.express(new Quantity(5000, meter)); // Quantity(5, kilometer)
206
206
  imperial.express(new Quantity(5000, meter)); // Quantity(3.107…, mile)
207
207
  ```
208
208
 
209
+ Under the hood, `express` just hands the system's units for that dimension to
210
+ `Quantity.best`, the same best-fit primitive you can call directly with whatever
211
+ units you like — handy when you want a custom shortlist rather than a whole
212
+ system. `best` picks the largest unit whose absolute magnitude is still ≥ 1
213
+ (falling back to the smallest when even that rounds below 1), so candidate order
214
+ doesn't matter:
215
+
216
+ ```ts
217
+ import { Quantity } from "measurable";
218
+ import { meter, kilometer, mile } from "measurable/dimensions";
219
+
220
+ new Quantity(5000, meter).best(meter, kilometer, mile); // Quantity(3.107…, mile)
221
+ new Quantity(1500, meter).best(meter, kilometer); // Quantity(1.5, kilometer)
222
+ new Quantity(500, meter).best(kilometer, mile); // Quantity(0.5, kilometer) — smallest fallback
223
+ ```
224
+
225
+ It requires at least one unit, and each must belong to the quantity's dimension
226
+ (otherwise `DimensionMismatchError`).
227
+
209
228
  ## Formatting output
210
229
 
211
230
  Each unit carries a canonical `symbol` (`"g"`, `"km"`, `"°C"`) and `plural`
@@ -328,7 +347,7 @@ new Quantity(6, mile).dividedBy(2); // Quantity(3, mile)
328
347
  Short aliases are available: **`add`** (`plus`), **`sub`** (`minus`), **`mul`**
329
348
  (`times`), **`div`** (`dividedBy`).
330
349
 
331
- Combining different dimensions throws `InvalidConversionError`. Note that adding
350
+ Combining different dimensions throws `DimensionMismatchError`. Note that adding
332
351
  **affine** units (e.g. temperatures) is mathematically defined but physically
333
352
  questionable, since it adds absolute points rather than a difference.
334
353
 
@@ -349,13 +368,13 @@ This is different from `.in(unit)`: `.in(milliliter)` only uses the *unit* on th
349
368
  right (giving `2000`), whereas `ratioTo` also uses the other quantity's
350
369
  **magnitude** (the `250`), so it answers "how many of *that quantity* fit in this
351
370
  one." It's the inverse of scalar `times` — `b.times(a.ratioTo(b))` reconstructs
352
- `a`. Comparing different dimensions throws `InvalidConversionError`.
371
+ `a`. Comparing different dimensions throws `DimensionMismatchError`.
353
372
 
354
373
  ## Comparison
355
374
 
356
375
  `equals`/`notEquals`/`lessThan`/`greaterThan`/`lessThanOrEqual`/`greaterThanOrEqual`
357
376
  compare two quantities (the other is converted into the receiver's unit first),
358
- returning a boolean. Comparing different dimensions throws `InvalidConversionError`.
377
+ returning a boolean. Comparing different dimensions throws `DimensionMismatchError`.
359
378
 
360
379
  ```ts
361
380
  import { Quantity } from "measurable";
@@ -381,7 +400,7 @@ comparator: `quantities.sort((a, b) => a.compareTo(b))`.
381
400
 
382
401
  `Quantity.min`/`max`/`sum` aggregate several quantities at once; `clamp` is an
383
402
  instance method that bounds one quantity to a range. Each converts operands as
384
- needed, so mixing dimensions throws `InvalidConversionError`.
403
+ needed, so mixing dimensions throws `DimensionMismatchError`.
385
404
 
386
405
  ```ts
387
406
  import { Quantity } from "measurable";
@@ -538,6 +557,7 @@ A passive handle, normally created via a dimension's builder methods rather than
538
557
  - `.negate()` / `.abs()` → `Quantity`
539
558
  - `.clamp(lower, upper)` → `Quantity` — bound to a range, in this unit
540
559
  - `.round(decimals?)` → `Quantity` — round the magnitude (default 0 decimals)
560
+ - `.best(...units)` → `Quantity` — re-express in the largest given unit whose absolute magnitude is still ≥ 1 (smallest as fallback); needs ≥ 1 unit, all of this dimension
541
561
  - `.equals(other)` / `.notEquals(other)` → `boolean` (aliases: `eq` / `ne`)
542
562
  - `.lessThan(other)` / `.greaterThan(other)` → `boolean` (aliases: `lt` / `gt`)
543
563
  - `.lessThanOrEqual(other)` / `.greaterThanOrEqual(other)` → `boolean` (aliases: `lte` / `gte`)
@@ -572,9 +592,17 @@ pass anywhere a magnitude or scale is accepted.
572
592
 
573
593
  ### Errors
574
594
 
575
- - `InvalidConversionError` units are from different dimensions
595
+ All extend the built-in `Error` (so existing `catch` blocks still catch them) and
596
+ are exported from `measurable`, letting you branch on the failure with
597
+ `instanceof`:
598
+
599
+ - `DimensionMismatchError` — an operation was attempted on units from different dimensions (converting, comparing, or combining them). Exported as `InvalidConversionError` too, a deprecated alias of the same class.
576
600
  - `UnknownUnitError` — a parsed token matches no unit
577
601
  - `AmbiguousUnitError` — a parsed token matches several units and no `prefer` was given
602
+ - `ParseError` — a string could not be parsed into a quantity at all
603
+ - `DuplicateUnitError` — a unit name already exists in its dimension
604
+ - `UnsupportedDimensionError` — a system has no units of a dimension to `express` in
605
+ - `ArgumentError` — an argument was invalid (e.g. `best()` with no units, or a non-integer / zero-denominator / non-finite `Rational`)
578
606
 
579
607
  ## Changelog
580
608
 
@@ -4,8 +4,7 @@ exports.AmbiguousUnitError = void 0;
4
4
  class AmbiguousUnitError extends Error {
5
5
  constructor(token, candidates) {
6
6
  const names = candidates.map((unit) => unit.name).join(", ");
7
- super(`Ambiguous unit "${token}": matches ${names}. ` +
8
- `Pass a preferred measurement system to disambiguate.`);
7
+ super(`Ambiguous unit "${token}": matches ${names}. Pass a preferred measurement system to disambiguate.`);
9
8
  }
10
9
  }
11
10
  exports.AmbiguousUnitError = AmbiguousUnitError;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Thrown when a caller passes an invalid argument — an empty required list, a
3
+ * value outside an allowed range, a wrong numeric type, and so on. Signals a
4
+ * mistake in the calling code rather than a recoverable runtime condition.
5
+ */
6
+ export declare class ArgumentError extends Error {
7
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ArgumentError = void 0;
4
+ /**
5
+ * Thrown when a caller passes an invalid argument — an empty required list, a
6
+ * value outside an allowed range, a wrong numeric type, and so on. Signals a
7
+ * mistake in the calling code rather than a recoverable runtime condition.
8
+ */
9
+ class ArgumentError extends Error {
10
+ }
11
+ exports.ArgumentError = ArgumentError;
@@ -0,0 +1,13 @@
1
+ import type { Unit } from "../lib/Unit";
2
+ /**
3
+ * Thrown when an operation is attempted on two units from different dimensions —
4
+ * converting, comparing, or combining them (e.g. `meter` and `liter`). Such
5
+ * units are dimensionally incompatible, so the operation has no meaning.
6
+ */
7
+ export declare class DimensionMismatchError extends Error {
8
+ constructor(from: Unit, to: Unit);
9
+ }
10
+ /** @deprecated Renamed to {@link DimensionMismatchError}. */
11
+ export declare const InvalidConversionError: typeof DimensionMismatchError;
12
+ /** @deprecated Renamed to {@link DimensionMismatchError}. */
13
+ export type InvalidConversionError = DimensionMismatchError;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidConversionError = exports.DimensionMismatchError = void 0;
4
+ /**
5
+ * Thrown when an operation is attempted on two units from different dimensions —
6
+ * converting, comparing, or combining them (e.g. `meter` and `liter`). Such
7
+ * units are dimensionally incompatible, so the operation has no meaning.
8
+ */
9
+ class DimensionMismatchError extends Error {
10
+ constructor(from, to) {
11
+ super(`Invalid conversion: ${from.name} to ${to.name}`);
12
+ }
13
+ }
14
+ exports.DimensionMismatchError = DimensionMismatchError;
15
+ /** @deprecated Renamed to {@link DimensionMismatchError}. */
16
+ exports.InvalidConversionError = DimensionMismatchError;
@@ -0,0 +1,5 @@
1
+ import type { Dimension } from "../lib/Dimension";
2
+ /** Thrown when defining a unit whose name already exists in its dimension. */
3
+ export declare class DuplicateUnitError extends Error {
4
+ constructor(name: string, dimension: Dimension);
5
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DuplicateUnitError = void 0;
4
+ /** Thrown when defining a unit whose name already exists in its dimension. */
5
+ class DuplicateUnitError extends Error {
6
+ constructor(name, dimension) {
7
+ super(`Duplicate unit name "${name}" in dimension "${dimension.name}"`);
8
+ }
9
+ }
10
+ exports.DuplicateUnitError = DuplicateUnitError;
@@ -0,0 +1,4 @@
1
+ /** Thrown when a string can't be parsed into a quantity (no magnitude/unit found). */
2
+ export declare class ParseError extends Error {
3
+ constructor(input: string);
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ParseError = void 0;
4
+ /** Thrown when a string can't be parsed into a quantity (no magnitude/unit found). */
5
+ class ParseError extends Error {
6
+ constructor(input) {
7
+ super(`Could not parse a quantity from "${input}"`);
8
+ }
9
+ }
10
+ exports.ParseError = ParseError;
@@ -0,0 +1,10 @@
1
+ import type { Dimension } from "../lib/Dimension";
2
+ import type { MeasurementSystem } from "../lib/MeasurementSystem";
3
+ /**
4
+ * Thrown when a measurement system is asked to express a quantity in a dimension
5
+ * for which it has no units (e.g. an imperial-only system and a metric-only
6
+ * dimension).
7
+ */
8
+ export declare class UnsupportedDimensionError extends Error {
9
+ constructor(system: MeasurementSystem, dimension: Dimension);
10
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnsupportedDimensionError = void 0;
4
+ /**
5
+ * Thrown when a measurement system is asked to express a quantity in a dimension
6
+ * for which it has no units (e.g. an imperial-only system and a metric-only
7
+ * dimension).
8
+ */
9
+ class UnsupportedDimensionError extends Error {
10
+ constructor(system, dimension) {
11
+ super(`Measurement system "${system.name}" has no "${dimension.name}" units to express in`);
12
+ }
13
+ }
14
+ exports.UnsupportedDimensionError = UnsupportedDimensionError;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export * from "./errors/AmbiguousUnitError";
2
- export * from "./errors/InvalidConversionError";
2
+ export * from "./errors/ArgumentError";
3
+ export * from "./errors/DimensionMismatchError";
4
+ export * from "./errors/DuplicateUnitError";
5
+ export * from "./errors/ParseError";
3
6
  export * from "./errors/UnknownUnitError";
7
+ export * from "./errors/UnsupportedDimensionError";
4
8
  export * from "./lib/Dimension";
5
9
  export * from "./lib/MeasurementSystem";
6
10
  export * from "./lib/Quantity";
package/dist/index.js CHANGED
@@ -15,8 +15,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./errors/AmbiguousUnitError"), exports);
18
- __exportStar(require("./errors/InvalidConversionError"), exports);
18
+ __exportStar(require("./errors/ArgumentError"), exports);
19
+ __exportStar(require("./errors/DimensionMismatchError"), exports);
20
+ __exportStar(require("./errors/DuplicateUnitError"), exports);
21
+ __exportStar(require("./errors/ParseError"), exports);
19
22
  __exportStar(require("./errors/UnknownUnitError"), exports);
23
+ __exportStar(require("./errors/UnsupportedDimensionError"), exports);
20
24
  __exportStar(require("./lib/Dimension"), exports);
21
25
  __exportStar(require("./lib/MeasurementSystem"), exports);
22
26
  __exportStar(require("./lib/Quantity"), exports);
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Dimension = void 0;
4
- const InvalidConversionError_1 = require("../errors/InvalidConversionError");
4
+ const DimensionMismatchError_1 = require("../errors/DimensionMismatchError");
5
+ const DuplicateUnitError_1 = require("../errors/DuplicateUnitError");
5
6
  const Rational_1 = require("./Rational");
6
7
  const Unit_1 = require("./Unit");
7
8
  /** The rational `0`. */
@@ -79,7 +80,7 @@ class Dimension {
79
80
  */
80
81
  convertRational(value, from, to) {
81
82
  if (!this.units.has(from) || !this.units.has(to)) {
82
- throw new InvalidConversionError_1.InvalidConversionError(from, to);
83
+ throw new DimensionMismatchError_1.DimensionMismatchError(from, to);
83
84
  }
84
85
  if (from === to) {
85
86
  return value;
@@ -105,7 +106,7 @@ class Dimension {
105
106
  define(name, { symbol, plural, aliases = [] }, transform) {
106
107
  for (const existing of this.units) {
107
108
  if (existing.name === name) {
108
- throw new Error(`Duplicate unit name "${name}" in dimension "${this.name}"`);
109
+ throw new DuplicateUnitError_1.DuplicateUnitError(name, this);
109
110
  }
110
111
  }
111
112
  const unit = new Unit_1.Unit({ name, dimension: this, symbol, plural, ...transform });
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MeasurementSystem = void 0;
4
- const scaleOf_1 = require("../utils/scaleOf");
4
+ const UnsupportedDimensionError_1 = require("../errors/UnsupportedDimensionError");
5
5
  /**
6
6
  * A measurement system (metric, imperial, US customary, …) is a cross-dimension
7
7
  * collection of units that share a real-world standard.
@@ -37,22 +37,11 @@ class MeasurementSystem {
37
37
  * to the smallest unit when even that rounds below 1).
38
38
  */
39
39
  express(quantity) {
40
- const candidates = this.in(quantity.unit.dimension).sort((a, b) => (0, scaleOf_1.scaleOf)(a) - (0, scaleOf_1.scaleOf)(b));
40
+ const candidates = this.in(quantity.unit.dimension);
41
41
  if (candidates.length === 0) {
42
- throw new Error(`Measurement system "${this.name}" has no ` +
43
- `"${quantity.unit.dimension.name}" units to express in`);
42
+ throw new UnsupportedDimensionError_1.UnsupportedDimensionError(this, quantity.unit.dimension);
44
43
  }
45
- const base = quantity.unit.toBase(quantity.magnitude);
46
- let chosen = candidates[0];
47
- for (const unit of candidates) {
48
- if (Math.abs(unit.fromBase(base)) >= 1) {
49
- chosen = unit;
50
- }
51
- else {
52
- break;
53
- }
54
- }
55
- return quantity.to(chosen);
44
+ return quantity.best(...candidates);
56
45
  }
57
46
  }
58
47
  exports.MeasurementSystem = MeasurementSystem;
@@ -67,6 +67,14 @@ export declare class Quantity {
67
67
  in(target: Unit): number;
68
68
  /** This quantity's exact magnitude expressed in `target`. */
69
69
  private inRational;
70
+ /**
71
+ * Re-express this quantity in the best-fit unit among `units`: the largest
72
+ * unit whose absolute magnitude is still at least 1 (falling back to the
73
+ * smallest unit when even that rounds below 1). Requires at least one unit,
74
+ * and each must belong to this quantity's dimension (else
75
+ * {@link DimensionMismatchError}).
76
+ */
77
+ best(...units: Unit[]): Quantity;
70
78
  /** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
71
79
  toString(): string;
72
80
  /**
@@ -103,7 +111,7 @@ export declare class Quantity {
103
111
  /**
104
112
  * Add another quantity, returned in *this* quantity's unit. The other operand
105
113
  * is converted into this unit first, so the two may use different units of the
106
- * same dimension (e.g. `mile.plus(km)`). Throws {@link InvalidConversionError}
114
+ * same dimension (e.g. `mile.plus(km)`). Throws {@link DimensionMismatchError}
107
115
  * if the operands belong to different dimensions.
108
116
  *
109
117
  * Note: for affine units (e.g. temperature) addition is mathematically defined
@@ -121,7 +129,7 @@ export declare class Quantity {
121
129
  * Divide this quantity by `other` of the same dimension, yielding the
122
130
  * dimensionless ratio between them — i.e. how many of `other` fit in this.
123
131
  * Unlike {@link in}, this accounts for `other`'s magnitude, not just its unit.
124
- * Throws {@link InvalidConversionError} across dimensions.
132
+ * Throws {@link DimensionMismatchError} across dimensions.
125
133
  */
126
134
  ratioTo(other: Quantity): number;
127
135
  /** Return this quantity with its magnitude negated. */
@@ -142,7 +150,7 @@ export declare class Quantity {
142
150
  div(divisor: number | Rational): Quantity;
143
151
  /**
144
152
  * Whether this quantity equals `other`, compared in this quantity's unit.
145
- * Throws {@link InvalidConversionError} if the operands belong to different
153
+ * Throws {@link DimensionMismatchError} if the operands belong to different
146
154
  * dimensions. Comparison is exact rational equality, so quantities that are
147
155
  * mathematically equal compare equal even if reaching them involved a
148
156
  * conversion that would have drifted in floating point.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Quantity = void 0;
4
4
  const assert_never_1 = require("assert-never");
5
5
  const AmbiguousUnitError_1 = require("../errors/AmbiguousUnitError");
6
+ const ArgumentError_1 = require("../errors/ArgumentError");
7
+ const ParseError_1 = require("../errors/ParseError");
6
8
  const UnknownUnitError_1 = require("../errors/UnknownUnitError");
7
9
  const scaleOf_1 = require("../utils/scaleOf");
8
10
  const Rational_1 = require("./Rational");
@@ -28,6 +30,29 @@ class Quantity {
28
30
  inRational(target) {
29
31
  return this.unit.dimension.convertRational(this.rational, this.unit, target);
30
32
  }
33
+ /**
34
+ * Re-express this quantity in the best-fit unit among `units`: the largest
35
+ * unit whose absolute magnitude is still at least 1 (falling back to the
36
+ * smallest unit when even that rounds below 1). Requires at least one unit,
37
+ * and each must belong to this quantity's dimension (else
38
+ * {@link DimensionMismatchError}).
39
+ */
40
+ best(...units) {
41
+ const candidates = [...units].sort((a, b) => (0, scaleOf_1.scaleOf)(a) - (0, scaleOf_1.scaleOf)(b));
42
+ if (candidates.length === 0) {
43
+ throw new ArgumentError_1.ArgumentError("Quantity.best requires at least one unit");
44
+ }
45
+ let chosen = candidates[0];
46
+ for (const unit of candidates) {
47
+ if (Math.abs(this.in(unit)) >= 1) {
48
+ chosen = unit;
49
+ }
50
+ else {
51
+ break;
52
+ }
53
+ }
54
+ return this.to(chosen);
55
+ }
31
56
  /** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
32
57
  toString() {
33
58
  return `${this.magnitude} ${this.unit.name}`;
@@ -87,7 +112,7 @@ class Quantity {
87
112
  /**
88
113
  * Add another quantity, returned in *this* quantity's unit. The other operand
89
114
  * is converted into this unit first, so the two may use different units of the
90
- * same dimension (e.g. `mile.plus(km)`). Throws {@link InvalidConversionError}
115
+ * same dimension (e.g. `mile.plus(km)`). Throws {@link DimensionMismatchError}
91
116
  * if the operands belong to different dimensions.
92
117
  *
93
118
  * Note: for affine units (e.g. temperature) addition is mathematically defined
@@ -113,7 +138,7 @@ class Quantity {
113
138
  * Divide this quantity by `other` of the same dimension, yielding the
114
139
  * dimensionless ratio between them — i.e. how many of `other` fit in this.
115
140
  * Unlike {@link in}, this accounts for `other`'s magnitude, not just its unit.
116
- * Throws {@link InvalidConversionError} across dimensions.
141
+ * Throws {@link DimensionMismatchError} across dimensions.
117
142
  */
118
143
  ratioTo(other) {
119
144
  return this.rational.dividedBy(other.inRational(this.unit)).toNumber();
@@ -159,7 +184,7 @@ class Quantity {
159
184
  }
160
185
  /**
161
186
  * Whether this quantity equals `other`, compared in this quantity's unit.
162
- * Throws {@link InvalidConversionError} if the operands belong to different
187
+ * Throws {@link DimensionMismatchError} if the operands belong to different
163
188
  * dimensions. Comparison is exact rational equality, so quantities that are
164
189
  * mathematically equal compare equal even if reaching them involved a
165
190
  * conversion that would have drifted in floating point.
@@ -257,7 +282,7 @@ class Quantity {
257
282
  }
258
283
  }
259
284
  if (!total || !finest) {
260
- throw new Error(`Could not parse a quantity from "${str}"`);
285
+ throw new ParseError_1.ParseError(str);
261
286
  }
262
287
  return total.to(finest);
263
288
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Rational = void 0;
4
+ const ArgumentError_1 = require("../errors/ArgumentError");
4
5
  /**
5
6
  * An exact rational number (`n / d`) used to make conversions between linear
6
7
  * and affine units lossless. Such conversions are inherently rational — a foot
@@ -23,7 +24,7 @@ class Rational {
23
24
  let n = toBigInt(numerator);
24
25
  let d = toBigInt(denominator);
25
26
  if (d === 0n) {
26
- throw new Error("Rational denominator cannot be zero");
27
+ throw new ArgumentError_1.ArgumentError("Rational denominator cannot be zero");
27
28
  }
28
29
  if (d < 0n) {
29
30
  n = -n;
@@ -48,11 +49,11 @@ class Rational {
48
49
  */
49
50
  static fromNumber(value) {
50
51
  if (!Number.isFinite(value)) {
51
- throw new Error(`Cannot derive a rational from ${value}`);
52
+ throw new ArgumentError_1.ArgumentError(`Cannot derive a rational from ${value}`);
52
53
  }
53
54
  const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(value.toString());
54
55
  if (!match) {
55
- throw new Error(`Cannot derive a rational from ${value}`);
56
+ throw new ArgumentError_1.ArgumentError(`Cannot derive a rational from ${value}`);
56
57
  }
57
58
  const [, sign, intPart, fracPart = "", expPart] = match;
58
59
  let n = BigInt(intPart + fracPart);
@@ -168,7 +169,7 @@ function toBigInt(value) {
168
169
  return value;
169
170
  }
170
171
  if (!Number.isInteger(value)) {
171
- throw new Error(`Rational numerator and denominator must be integers; got ${value}`);
172
+ throw new ArgumentError_1.ArgumentError(`Rational numerator and denominator must be integers; got ${value}`);
172
173
  }
173
174
  return BigInt(value);
174
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "measurable",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Convert between units of measurement with custom and built-in systems",
5
5
  "keywords": [
6
6
  "conversion",
@@ -59,5 +59,5 @@
59
59
  "typecheck": "tsc --noEmit",
60
60
  "test": "vitest run"
61
61
  },
62
- "readme": "# measurable\n\nConvert between units of measurement, with batteries-included common units and\nfirst-class support for defining your own.\n\n- **Exact, no drift** — magnitudes are held as exact rational numbers and\n conversions run in rational arithmetic, collapsing to a float only when you\n read `.magnitude`. So `foot → inch` is exactly `12` (not `12.000000000000002`),\n and chains and round trips like `liter → gallon → liter` come back to exactly\n what you started with.\n- **No redundant factors** — each unit defines a single transform to its\n dimension's base unit; reverse conversions are derived, never stored, so they\n can't fall out of sync.\n- **Free chaining** — any unit converts to any other in the same dimension\n (e.g. `mile → inch`) without you defining every pair.\n- **Affine units** — temperature scales (°C/°F/K) and anything else needing an\n offset, not just a scale factor.\n- **Two orthogonal ideas** — a **dimension** decides what _can_ convert; a\n **measurement system** (metric/imperial/US) is a tag that never gates\n conversion but powers filtering, formatting, and parse disambiguation.\n\n## Installation\n\n```sh\nnpm install measurable\n```\n\n## Entry points\n\nThe package is split into three import paths so the core stays lean:\n\n| Import | What you get |\n| -------------------------- | ------------------------------------------------------------------------- |\n| `measurable` | The building blocks: `Quantity`, `Dimension`, `MeasurementSystem`, `Unit`, `Rational`, errors |\n| `measurable/dimensions` | Predefined dimensions and their units (`length`, `meter`, `volume`, …) |\n| `measurable/systems` | Predefined measurement systems (`metric`, `imperial`, `usCustomary`) |\n\n## Quick start\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { meter, mile, foot, inch, celsius, fahrenheit } from \"measurable/dimensions\";\n\n// Convert: `.to()` returns a Quantity, `.in()` returns a raw number.\nnew Quantity(5, mile).to(meter).magnitude; // 8046.72\nnew Quantity(5, mile).in(meter); // 8046.72\n\n// Affine scales work the same way.\nnew Quantity(100, celsius).in(fahrenheit); // 212\n\n// Conversions are exact: magnitudes are rationals under the hood.\nnew Quantity(1, foot).in(inch); // 12 (not 12.000000000000002)\nnew Quantity(1, foot).to(inch).to(foot).magnitude; // 1 (exact round trip)\n```\n\n## Concepts\n\n- **`Dimension`** — a kind of measurable quantity (length, volume, mass, …). It\n owns a canonical **base unit** and is where all conversion happens. A unit\n belongs to exactly one dimension.\n- **`Unit`** — a name plus a transform into its dimension's base unit: an exact\n rational `scale`/`offset` for linear and affine units, or an arbitrary\n function pair for `custom` ones. Created through a dimension's builder methods.\n- **`Quantity`** — a magnitude paired with a unit (e.g. `5 kilometer`). The\n magnitude is held as an exact **`Rational`**; `.magnitude` reads it as a\n `number`.\n- **`Rational`** — an exact rational number (`n / d` over `bigint`s) used for the\n lossless arithmetic above. You rarely construct one directly, but can pass one\n anywhere a magnitude or scale is expected.\n- **`MeasurementSystem`** — a cross-dimension tag (metric/imperial/…). A unit can\n belong to many; membership is optional and never affects whether a conversion\n is allowed.\n\n## Exact arithmetic\n\nA linear conversion is inherently rational — a foot is exactly `3048/10000` m and\nan inch exactly `254/10000` m, so a foot is exactly `12` inches. Storing those\nratios as binary floats and routing values through the base unit loses that\n(`1 foot → inch` would give `12.000000000000002`).\n\nInstead, each `Quantity` keeps its magnitude as an exact `Rational`, and\nconversions/arithmetic run in rational arithmetic, collapsing to a `number` only\nwhen you read `.magnitude` (or call `.in()`). Because nothing is collapsed\nmid-chain, conversions compose without accumulating drift:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { liter, usGallon } from \"measurable/dimensions\";\n\n// A round trip through an awkward ratio still lands exactly on 7.\nnew Quantity(7, liter).to(usGallon).to(liter).magnitude; // 7\n\n// .rational exposes the underlying exact value (always in lowest terms).\nnew Quantity(7, liter).to(usGallon).rational; // Rational 125000000/67596639\n```\n\nThis is exact for linear and affine units. A conversion that passes through a\nnon-linear `custom` unit (e.g. a logarithmic scale) necessarily uses floating\npoint and recaptures the result as a rational, so it is best-effort there.\n\n## Built-in dimensions\n\nImport any dimension or unit from `measurable/dimensions`:\n\n| Dimension | Base | Units (a selection) |\n| -------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `length` | `meter` | `kilometer`, `centimeter`, `millimeter`, `inch`, `foot`, `yard`, `mile` |\n| `area` | `squareMeter` | `squareKilometer`, `hectare`, `are`, `squareInch`, `squareFoot`, `squareYard`, `acre`, `squareMile` |\n| `volume` | `liter` | `milliliter`, `us*`/`imperial*` `Gallon`/`Quart`/`Pint`/`Gill`/`FluidOunce`, `cup`, `tablespoon`, `teaspoon` |\n| `mass` | `gram` | `kilogram`, `milligram`, `tonne`, `pound`, `ounce`, `stone`, `shortTon`, `longTon` |\n| `time` | `second` | `millisecond`, `minute`, `hour`, `day`, `week` |\n| `temperature` | `kelvin` | `celsius`, `fahrenheit` |\n| `angle` | `radian` | `degree`, `gradian`, `turn` |\n| `force` | `newton` | `kilonewton`, `dyne`, `poundForce`, `kilogramForce` |\n| `energy` | `joule` | `kilojoule`, `wattHour`, `kilowattHour`, `calorie`, `kilocalorie`, `britishThermalUnit` |\n| `power` | `watt` | `kilowatt`, `megawatt`, `horsepower`, `metricHorsepower` |\n| `pressure` | `pascal` | `kilopascal`, `bar`, `millibar`, `atmosphere`, `torr`, `psi`, `inchOfMercury`, `inchOfWater` |\n| `frequency` | `hertz` | `kilohertz`, `megahertz`, `gigahertz`, `terahertz` |\n| `data` | `bit` | `byte`, `nibble`, `kilobyte`…`petabyte` (SI), `kibibyte`…`pebibyte` (IEC) |\n| `illuminance` | `lux` | `kilolux`, `millilux`, `footCandle`, `phot` |\n| `luminance` | `candelaPerSquareMeter` | `nit`, `stilb` |\n| `luminousIntensity` | `candela` | `kilocandela`, `millicandela`, `candlepower`, `hefnerkerze` |\n\nThe metric units carry the **full SI prefix ladder** (yotta → yocto), generated for\nyou: the SI-based dimensions (`length`, `mass`, `volume`, `force`, `energy`, `power`,\n`pressure`, `frequency`, `illuminance`, `luminousIntensity`) get every prefix (so\n`kilogram` itself is just the kilo-prefixed gram), while `time` and `angle` get the\nfractional prefixes only (e.g. `millisecond`, `microradian`). Every generated rung\nparses from its symbol (`dm`, `hm`, `Mg`, `kL`, `ns`, `GHz`, `kPa`, and `µm`/`um` for\nmicro) and converts like any other unit. `data` additionally carries the **IEC binary**\nmultiples (`kibibyte`, `mebibyte`, … = 1024-based) alongside the SI decimal ones\n(`kilobyte`, … = 1000-based). You can apply the same ladder to your own dimensions with\n`definePrefixed` (see below).\n\n### Prefixed unit exports\n\nEach prefixed dimension exports a **record** holding the _complete_ generated ladder\nkeyed by name (the return value of `definePrefixed`), plus a **curated subset of those\nsame units as individual named exports** for convenience. Reach any rung that isn't\nexported individually through the record — e.g. `metricLength.gigameter`,\n`metricMass.exagram` — or by parsing its symbol.\n\n| Dimension | Record export(s) | Individually exported units |\n| ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |\n| `length` | `metricLength` | `kilometer`, `hectometer`, `decameter`, `decimeter`, `centimeter`, `millimeter`, `micrometer`, `nanometer` |\n| `mass` | `metricMass` | `kilogram`, `megagram`, `hectogram`, `decagram`, `decigram`, `centigram`, `milligram`, `microgram`, `nanogram` |\n| `volume` | `metricVolume` | `kiloliter`, `hectoliter`, `decaliter`, `deciliter`, `centiliter`, `milliliter` |\n| `time` | `metricTime` | `millisecond`, `microsecond`, `nanosecond`, `picosecond` |\n| `angle` | `metricAngle` | `milliradian`, `microradian` |\n| `force` | `metricForce` | `meganewton`, `kilonewton`, `millinewton`, `micronewton` |\n| `energy` | `metricEnergy`, `metricWattHour` | `kilojoule`, `megajoule`, `gigajoule`, `millijoule`; `kilowattHour`, `megawattHour`, `gigawattHour` |\n| `power` | `metricPower` | `kilowatt`, `megawatt`, `gigawatt`, `terawatt`, `milliwatt` |\n| `pressure` | `metricPressure` | `kilopascal`, `hectopascal`, `megapascal`, `gigapascal` |\n| `frequency` | `metricFrequency` | `kilohertz`, `megahertz`, `gigahertz`, `terahertz`, `petahertz`, `millihertz` |\n| `illuminance` | `metricIlluminance` | `kilolux`, `millilux`, `microlux` |\n| `luminousIntensity` | `metricLuminousIntensity` | `kilocandela`, `millicandela`, `microcandela` |\n| `data` | `dataMultiples` | `kilobit`/`kilobyte` … `petabit`/`petabyte` (SI), `kibibit`/`kibibyte` … `pebibit`/`pebibyte` (IEC) |\n\n```ts\nimport { metricLength, kilometer } from \"measurable/dimensions\";\n\nkilometer === metricLength.kilometer; // true — the named export is the same unit\nmetricLength.gigameter; // a rung not exported by name, reached via the record\n```\n\n## Built-in measurement systems\n\nImport `metric`, `imperial`, or `usCustomary` from `measurable/systems`.\n\n```ts\nimport { foot } from \"measurable/dimensions\";\nimport { imperial, usCustomary, metric } from \"measurable/systems\";\n\nimperial.has(foot); // true — a unit can belong to several systems\nusCustomary.has(foot); // true\nmetric.has(foot); // false\n```\n\nMembership spans every dimension: SI units (`squareMeter`, `pascal`, `watt`, `joule`,\n`hertz`, `lux`, `candela`, and their prefix ladders) are tagged into `metric`, while the\ncustomary ones (`squareFoot`, `acre`, `psi`, `horsepower`, `britishThermalUnit`,\n`footCandle`, `candlepower`) belong to both `imperial` and `usCustomary`. Units tied to\nno real-world standard (e.g. `atmosphere`, `torr`, and the `data` multiples) stay\nuntagged — they still convert, they just won't appear under any system.\n\n### Listing units in a system\n\n```ts\nimport { length } from \"measurable/dimensions\";\nimport { metric } from \"measurable/systems\";\n\nmetric.in(length).map((u) => u.name); // [\"meter\", \"kilometer\", \"centimeter\", \"millimeter\"]\n```\n\n### Best-fit formatting\n\n`express` re-expresses a quantity in a system's most readable unit (the largest\nunit whose magnitude is still ≥ 1):\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { meter } from \"measurable/dimensions\";\nimport { metric, imperial } from \"measurable/systems\";\n\nmetric.express(new Quantity(5000, meter)); // Quantity(5, kilometer)\nimperial.express(new Quantity(5000, meter)); // Quantity(3.107…, mile)\n```\n\n## Formatting output\n\nEach unit carries a canonical `symbol` (`\"g\"`, `\"km\"`, `\"°C\"`) and `plural`\n(`\"grams\"`, `\"kilometers\"`) alongside its `name`, so a `Quantity` can be rendered the\nway you want:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { gram } from \"measurable/dimensions\";\n\nnew Quantity(5, gram).toString(); // \"5 gram\" (always the bare name)\nnew Quantity(5, gram).format(); // \"5 grams\" (magnitude-aware)\nnew Quantity(1, gram).format(); // \"1 gram\" (singular at ±1)\nnew Quantity(5, gram).format({ unit: \"symbol\" }); // \"5 g\"\nnew Quantity(5, gram).format({ unit: \"name\" }); // \"5 gram\"\nnew Quantity(5, gram).format({ unit: \"plural\" }); // \"5 grams\"\n\n// Localize the magnitude with locale / numberFormat (passed to toLocaleString):\nnew Quantity(1234.5, meter).format({ locale: \"de-DE\" }); // \"1.234,5 meters\"\nnew Quantity(1.23456, meter).format({ numberFormat: { maximumFractionDigits: 2 } }); // \"1.23 meters\"\nnew Quantity(1234.5, kilometer).format({ locale: \"de-DE\", unit: \"symbol\" }); // \"1.234,5 km\"\n```\n\n`toString()` is intentionally stable (`\"<magnitude> <name>\"`). `format(options?)` is the\nflexible one: `unit` defaults to `\"auto\"` (singular `name` at ±1, otherwise `plural`) and\naccepts `\"name\"`, `\"plural\"`, or `\"symbol\"`. When a unit has no `symbol`/`plural`, those\nmodes fall back to its `name`.\n\nThe magnitude is rendered with `Number.prototype.toLocaleString`. Pass `locale` (a BCP 47\nlocale or array) and/or `numberFormat` (`Intl.NumberFormatOptions` — precision via\n`maximumFractionDigits`, grouping, `style`, …) to control it; with neither set, the\nruntime's default locale is used. Use `round(decimals)` to trim the magnitude first\n(`new Quantity(1.6213, mile).round(2)` → `1.62 mile`).\n\nWhen a single string won't do — e.g. styling the magnitude in a React component — use\n`formatParts(options?)`, which takes the same options but returns the rendered\n`{ magnitude, unit }` as separate strings for you to assemble:\n\n```tsx\nconst { magnitude, unit } = new Quantity(1234.5, kilometer).formatParts({ locale: \"de-DE\" });\n// { magnitude: \"1.234,5\", unit: \"kilometers\" }\nreturn <><b>{magnitude}</b> {unit}</>;\n```\n\n### Internationalization\n\n`format()` localizes the **magnitude** (via `locale`/`numberFormat`), but the **label** it\nappends — `symbol`/`plural` — is **English/canonical** convenience data, not a localization\nsystem: a single plural string can't model languages with several plural forms, and the\nnames themselves are English. For a fully localized label, delegate to `Intl.NumberFormat`,\nwhose `style: \"unit\"` localizes **and** pluralizes a curated set of units for you:\n\n```ts\nconst q = new Quantity(5, kilometer);\nnew Intl.NumberFormat(\"de\", { style: \"unit\", unit: \"kilometer\" }).format(q.magnitude);\n// \"5 Kilometer\"\nnew Intl.NumberFormat(\"fr\", { style: \"unit\", unit: \"kilometer\", unitDisplay: \"short\" })\n .format(q.magnitude); // \"5 km\"\n```\n\n## Parsing strings\n\n`Quantity.parse(input, dimension, options?)` reads a string into a `Quantity`.\nCompound inputs are summed and returned in the finest unit present:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { length, time } from \"measurable/dimensions\";\n\nQuantity.parse(\"1km\", length); // Quantity(1, kilometer)\nQuantity.parse(\"5 hr\", time); // Quantity(5, hour)\nQuantity.parse(\"5hr 20min\", time); // Quantity(320, minute)\n```\n\n### Ambiguous aliases\n\nSome names mean different things in different systems — a US gallon (3.785 L) is\nnot an imperial gallon (4.546 L), and `ton` could be short or long. These are\ndistinct units that share an alias, so an unqualified parse throws; pass a\n`prefer`red system to disambiguate:\n\n```ts\nimport { Quantity, AmbiguousUnitError } from \"measurable\";\nimport { volume, mass } from \"measurable/dimensions\";\nimport { usCustomary, imperial } from \"measurable/systems\";\n\nQuantity.parse(\"1 gallon\", volume); // throws AmbiguousUnitError\nQuantity.parse(\"1 gallon\", volume, { prefer: usCustomary }).unit.name; // \"usGallon\"\nQuantity.parse(\"1 ton\", mass, { prefer: imperial }).unit.name; // \"longTon\"\n```\n\nConversion itself is governed only by the dimension, so cross-system conversions\nalways work regardless of tags:\n\n```ts\nimport { shortTon, tonne } from \"measurable/dimensions\";\n\nnew Quantity(1, shortTon).in(tonne); // 0.90718474\n```\n\n## Arithmetic\n\nQuantities can be combined. `plus`/`minus` take another `Quantity` (converted into\nthe receiver's unit first, so the operands may use different units of the same\ndimension); `times`/`dividedBy` apply a dimensionless scalar (a `number` or a\n`Rational`), and `negate`/`abs` transform the magnitude. All return a **new**\n`Quantity` in the receiver's unit and leave the operands untouched. Like\nconversions, the arithmetic is exact — `q.times(3).dividedBy(3)` returns `q`.\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { kilometer, mile } from \"measurable/dimensions\";\n\nnew Quantity(1, mile).plus(new Quantity(1, kilometer)); // Quantity(1.6213…, mile)\nnew Quantity(1, mile).minus(new Quantity(1, kilometer)); // Quantity(0.3786…, mile)\nnew Quantity(2, mile).times(3); // Quantity(6, mile)\nnew Quantity(6, mile).dividedBy(2); // Quantity(3, mile)\n```\n\nShort aliases are available: **`add`** (`plus`), **`sub`** (`minus`), **`mul`**\n(`times`), **`div`** (`dividedBy`).\n\nCombining different dimensions throws `InvalidConversionError`. Note that adding\n**affine** units (e.g. temperatures) is mathematically defined but physically\nquestionable, since it adds absolute points rather than a difference.\n\n## Ratios\n\n`ratioTo` divides two quantities of the **same dimension** and returns a plain\n(dimensionless) number — _how many of one fit in the other_:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { liter, milliliter } from \"measurable/dimensions\";\n\n// How many 250 mL servings are in a 2 L bottle?\nnew Quantity(2, liter).ratioTo(new Quantity(250, milliliter)); // 8\n```\n\nThis is different from `.in(unit)`: `.in(milliliter)` only uses the *unit* on the\nright (giving `2000`), whereas `ratioTo` also uses the other quantity's\n**magnitude** (the `250`), so it answers \"how many of *that quantity* fit in this\none.\" It's the inverse of scalar `times` — `b.times(a.ratioTo(b))` reconstructs\n`a`. Comparing different dimensions throws `InvalidConversionError`.\n\n## Comparison\n\n`equals`/`notEquals`/`lessThan`/`greaterThan`/`lessThanOrEqual`/`greaterThanOrEqual`\ncompare two quantities (the other is converted into the receiver's unit first),\nreturning a boolean. Comparing different dimensions throws `InvalidConversionError`.\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { kilometer, meter } from \"measurable/dimensions\";\n\nnew Quantity(1, kilometer).equals(new Quantity(1000, meter)); // true\nnew Quantity(1, meter).lessThan(new Quantity(1, kilometer)); // true\nnew Quantity(1, kilometer).greaterThan(new Quantity(1, meter)); // true\n```\n\nShort aliases: **`eq`** (`equals`), **`ne`** (`notEquals`), **`lt`** (`lessThan`),\n**`gt`** (`greaterThan`), **`lte`** (`lessThanOrEqual`), **`gte`**\n(`greaterThanOrEqual`). Comparison is exact rational comparison, so quantities\nthat are mathematically equal compare equal even when reaching them involved a\nconversion that would have drifted in floating point — e.g.\n`new Quantity(7, liter).to(usGallon).to(liter).equals(new Quantity(7, liter))` is\n`true`.\n\n`compareTo(other)` returns `-1`, `0`, or `1`, suitable as an `Array#sort`\ncomparator: `quantities.sort((a, b) => a.compareTo(b))`.\n\n## Combining quantities\n\n`Quantity.min`/`max`/`sum` aggregate several quantities at once; `clamp` is an\ninstance method that bounds one quantity to a range. Each converts operands as\nneeded, so mixing dimensions throws `InvalidConversionError`.\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { kilometer, meter } from \"measurable/dimensions\";\n\nconst a = new Quantity(1, kilometer);\nconst b = new Quantity(500, meter);\n\nQuantity.min(a, b); // Quantity(500, meter) — the smaller\nQuantity.max(a, b); // Quantity(1, kilometer) — the larger\nQuantity.sum(a, b); // Quantity(1.5, kilometer) — total, in a's unit\nb.clamp(a, new Quantity(2, kilometer)); // b bounded to [a, 2 km], in b's unit\n```\n\n## Defining your own units\n\nCreate a `Dimension` and add units through its builder methods. `scale` is how\nmany base units make up one of the unit being defined. The optional final argument\nis a definition object — `{ symbol?, plural?, aliases? }` — whose `symbol` and\n`plural` feed `format()` and, like `aliases`, are also registered for parsing.\n\n```ts\nimport { Dimension, Quantity } from \"measurable\";\n\nconst data = new Dimension(\"data\");\nconst byte = data.base(\"byte\", { symbol: \"B\", plural: \"bytes\" }); // base unit (identity)\nconst kilobyte = data.unit(\"kilobyte\", 1024, { symbol: \"KB\", plural: \"kilobytes\" });\nconst megabyte = data.unit(\"megabyte\", 1024 ** 2, { symbol: \"MB\", plural: \"megabytes\" });\n\nnew Quantity(2, megabyte).in(kilobyte); // 2048\nnew Quantity(2, megabyte).format(); // \"2 megabytes\"\nnew Quantity(2, megabyte).format({ unit: \"symbol\" }); // \"2 MB\"\n```\n\nA numeric `scale` is read as the exact decimal you wrote (`0.0254` → `254/10000`),\nwhich is exact for any terminating decimal. For a ratio a decimal **can't**\nrepresent exactly — e.g. `5/9` — pass a `Rational` so it stays exact:\n\n```ts\nimport { Dimension, Rational } from \"measurable\";\n\nconst ratio = new Dimension(\"ratio\");\nratio.base(\"whole\");\nconst third = ratio.unit(\"third\", new Rational(1, 3)); // exact, not 0.3333…\n```\n\n### Affine units (offset, not just scale)\n\n```ts\nimport { Dimension, Rational } from \"measurable\";\n\nconst temperature = new Dimension(\"temperature\");\nconst kelvin = temperature.base(\"kelvin\", { symbol: \"K\" });\n// value_in_base = value * scale + offset\nconst celsius = temperature.affine(\"celsius\", { scale: 1, offset: 273.15 }, {\n symbol: \"°C\",\n aliases: [\"C\"],\n});\n// Fahrenheit's 5/9 isn't a terminating decimal — give it (and the derived\n// offset) as exact Rationals so conversions round-trip without drift.\nconst scale = new Rational(5, 9);\nconst fahrenheit = temperature.affine(\n \"fahrenheit\",\n { scale, offset: Rational.from(273.15).minus(new Rational(32).times(scale)) },\n { symbol: \"°F\", aliases: [\"F\"] },\n);\n```\n\n### Fully custom transforms\n\nFor anything non-linear, provide an explicit inverse pair:\n\n```ts\nconst dim = new Dimension(\"custom\");\ndim.base(\"base\");\ndim.custom(\"squared\", {\n toBase: (x) => x * x,\n fromBase: (x) => Math.sqrt(x),\n});\n```\n\n### Generating SI prefixes\n\n`definePrefixed` adds the metric prefix ladder to a reference **unit** and returns the\ncreated units keyed by name (skipping any name that already exists). It reads the\nreference's `name`, `symbol`, and scale straight off the unit (via `scaleOf`), so each\ngenerated unit gets a derived symbol (`b` → `kb`) and plural too — even when the\nreference isn't the base unit. Pass `SI_SUBMULTIPLE_PREFIXES` to generate fractions only.\n\n```ts\nimport { Dimension, Quantity, definePrefixed } from \"measurable\";\n\nconst data = new Dimension(\"data\");\nconst bit = data.base(\"bit\", { symbol: \"b\", plural: \"bits\" });\nconst prefixed = definePrefixed(data, bit);\n\nnew Quantity(1, prefixed.kilobit).in(bit); // 1000 (SI kilo = 1e3)\nprefixed.kilobit.symbol; // \"kb\"\nnew Quantity(5, prefixed.kilobit).format({ unit: \"symbol\" }); // \"5 kb\"\n```\n\n### Tagging units into a measurement system\n\n```ts\nimport { MeasurementSystem } from \"measurable\";\n\nconst si = new MeasurementSystem(\"si\").add(byte, kilobyte, megabyte);\nsi.has(kilobyte); // true\n```\n\n## API reference\n\n### `Dimension`\n\n- `new Dimension(name)`\n- `.base(name, def?)` — define the canonical base unit\n- `.unit(name, scale, def?)` — linear unit (`scale` base units per unit; `number | Rational`)\n- `.affine(name, { scale, offset }, def?)` — linear with additive offset (each `number | Rational`)\n- `.custom(name, { toBase, fromBase }, def?)` — arbitrary inverse pair, for non-linear units\n- `.convert(value, from, to)` — convert a raw `number` between two of its units\n- `.convertRational(value, from, to)` → `Rational` — exact conversion between two of its units\n- `.get(token)` — units matching a name/alias (`Unit[] | undefined`)\n- `.has(unit)`, `.units`, `.baseUnit`\n\n`def` is an optional `UnitDef`: `{ symbol?, plural?, aliases? }`. All three are\nregistered as parse tokens; `symbol`/`plural` are additionally stored on the `Unit`.\n\n### `Unit`\n\nA passive handle, normally created via a dimension's builder methods rather than\n`new Unit` directly. Read-only properties:\n\n- `.name` — the unit's canonical name\n- `.symbol?` — canonical symbol (e.g. `\"g\"`, `\"km\"`), if declared\n- `.plural?` — plural name (e.g. `\"grams\"`), if declared\n- `.dimension` — the `Dimension` it belongs to\n- `.linear` → `{ scale: Rational; offset: Rational } | undefined` — the exact transform for linear/affine units (`undefined` for `custom` ones)\n- `.toBase(value)` → `number` — convert a value in this unit to base units\n- `.fromBase(value)` → `number` — convert a value in base units to this unit\n\n### `Quantity`\n\n- `new Quantity(magnitude, unit)` — `magnitude` is a `number | Rational`; throws on a non-finite `number`\n- `.magnitude` → `number` — getter, derived from `.rational`\n- `.rational` → `Rational` — the exact magnitude (source of truth)\n- `.to(target)` → `Quantity`\n- `.in(target)` → `number`\n- `.toString()` → `string` — stable `\"<magnitude> <name>\"`, e.g. `\"5 kilometer\"`\n- `.format({ unit?, locale?, numberFormat? })` → `string` — `unit`: `\"auto\"` (default, magnitude-aware) / `\"name\"` / `\"plural\"` / `\"symbol\"`; `locale` + `numberFormat` localize the magnitude via `toLocaleString`\n- `.formatParts(options?)` → `{ magnitude, unit }` — same options as `.format`, but returns the rendered pieces separately for custom assembly (e.g. JSX)\n- `.plus(other)` / `.minus(other)` → `Quantity` — add/subtract another quantity (aliases: `add` / `sub`)\n- `.times(factor)` / `.dividedBy(divisor)` → `Quantity` — scale by a `number | Rational` (aliases: `mul` / `div`)\n- `.ratioTo(other)` → `number` — dimensionless ratio (how many of `other` fit in this)\n- `.negate()` / `.abs()` → `Quantity`\n- `.clamp(lower, upper)` → `Quantity` — bound to a range, in this unit\n- `.round(decimals?)` → `Quantity` — round the magnitude (default 0 decimals)\n- `.equals(other)` / `.notEquals(other)` → `boolean` (aliases: `eq` / `ne`)\n- `.lessThan(other)` / `.greaterThan(other)` → `boolean` (aliases: `lt` / `gt`)\n- `.lessThanOrEqual(other)` / `.greaterThanOrEqual(other)` → `boolean` (aliases: `lte` / `gte`)\n- `.compareTo(other)` → `-1 | 0 | 1` — sort comparator\n- `.isZero()` / `.isPositive()` / `.isNegative()` → `boolean`\n- `Quantity.min(...quantities)` / `Quantity.max(...quantities)` / `Quantity.sum(...quantities)` → `Quantity`\n- `Quantity.parse(input, dimension, { prefer? })` → `Quantity`\n\n### `Rational`\n\nAn exact rational number (`n / d`), stored as `bigint`s in lowest terms with a\npositive denominator. Immutable; every operation returns a new `Rational`. Used\ninternally for lossless conversions and arithmetic, but you can construct one to\npass anywhere a magnitude or scale is accepted.\n\n- `new Rational(numerator, denominator?)` — from integers (`bigint | number`; denominator defaults to `1`). Throws on a non-integer `number` or a zero denominator.\n- `Rational.from(value)` — coerce a `number | Rational` (a `number` is read as its exact terminating decimal, e.g. `0.0254` → `254/10000`)\n- `.n` / `.d` → `bigint` — numerator and denominator\n- `.plus(other)` / `.minus(other)` / `.times(other)` / `.dividedBy(other)` → `Rational` (aliases: `add` / `sub` / `mul` / `div`)\n- `.negate()` / `.abs()` → `Rational`\n- `.equals(other)` → `boolean` (alias: `eq`)\n- `.compare(other)` → `-1 | 0 | 1`\n- `.sign()` → `-1 | 0 | 1`\n- `.toNumber()` → `number` — collapse to the nearest `number`\n\n### `MeasurementSystem`\n\n- `new MeasurementSystem(name)`\n- `.add(...units)`, `.has(unit)`\n- `.in(dimension)` → `Unit[]`\n- `.express(quantity)` → `Quantity`\n\n### Errors\n\n- `InvalidConversionError` — units are from different dimensions\n- `UnknownUnitError` — a parsed token matches no unit\n- `AmbiguousUnitError` — a parsed token matches several units and no `prefer` was given\n\n## Changelog\n\nSee [CHANGELOG.md](https://github.com/mhuggins/measurable/blob/main/CHANGELOG.md)\nfor release notes. Note that v2.0.0 is a breaking release — see its entry for the\nmigration details.\n\n## License\n\nISC\n"
62
+ "readme": "# measurable\n\nConvert between units of measurement, with batteries-included common units and\nfirst-class support for defining your own.\n\n- **Exact, no drift** — magnitudes are held as exact rational numbers and\n conversions run in rational arithmetic, collapsing to a float only when you\n read `.magnitude`. So `foot → inch` is exactly `12` (not `12.000000000000002`),\n and chains and round trips like `liter → gallon → liter` come back to exactly\n what you started with.\n- **No redundant factors** — each unit defines a single transform to its\n dimension's base unit; reverse conversions are derived, never stored, so they\n can't fall out of sync.\n- **Free chaining** — any unit converts to any other in the same dimension\n (e.g. `mile → inch`) without you defining every pair.\n- **Affine units** — temperature scales (°C/°F/K) and anything else needing an\n offset, not just a scale factor.\n- **Two orthogonal ideas** — a **dimension** decides what _can_ convert; a\n **measurement system** (metric/imperial/US) is a tag that never gates\n conversion but powers filtering, formatting, and parse disambiguation.\n\n## Installation\n\n```sh\nnpm install measurable\n```\n\n## Entry points\n\nThe package is split into three import paths so the core stays lean:\n\n| Import | What you get |\n| -------------------------- | ------------------------------------------------------------------------- |\n| `measurable` | The building blocks: `Quantity`, `Dimension`, `MeasurementSystem`, `Unit`, `Rational`, errors |\n| `measurable/dimensions` | Predefined dimensions and their units (`length`, `meter`, `volume`, …) |\n| `measurable/systems` | Predefined measurement systems (`metric`, `imperial`, `usCustomary`) |\n\n## Quick start\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { meter, mile, foot, inch, celsius, fahrenheit } from \"measurable/dimensions\";\n\n// Convert: `.to()` returns a Quantity, `.in()` returns a raw number.\nnew Quantity(5, mile).to(meter).magnitude; // 8046.72\nnew Quantity(5, mile).in(meter); // 8046.72\n\n// Affine scales work the same way.\nnew Quantity(100, celsius).in(fahrenheit); // 212\n\n// Conversions are exact: magnitudes are rationals under the hood.\nnew Quantity(1, foot).in(inch); // 12 (not 12.000000000000002)\nnew Quantity(1, foot).to(inch).to(foot).magnitude; // 1 (exact round trip)\n```\n\n## Concepts\n\n- **`Dimension`** — a kind of measurable quantity (length, volume, mass, …). It\n owns a canonical **base unit** and is where all conversion happens. A unit\n belongs to exactly one dimension.\n- **`Unit`** — a name plus a transform into its dimension's base unit: an exact\n rational `scale`/`offset` for linear and affine units, or an arbitrary\n function pair for `custom` ones. Created through a dimension's builder methods.\n- **`Quantity`** — a magnitude paired with a unit (e.g. `5 kilometer`). The\n magnitude is held as an exact **`Rational`**; `.magnitude` reads it as a\n `number`.\n- **`Rational`** — an exact rational number (`n / d` over `bigint`s) used for the\n lossless arithmetic above. You rarely construct one directly, but can pass one\n anywhere a magnitude or scale is expected.\n- **`MeasurementSystem`** — a cross-dimension tag (metric/imperial/…). A unit can\n belong to many; membership is optional and never affects whether a conversion\n is allowed.\n\n## Exact arithmetic\n\nA linear conversion is inherently rational — a foot is exactly `3048/10000` m and\nan inch exactly `254/10000` m, so a foot is exactly `12` inches. Storing those\nratios as binary floats and routing values through the base unit loses that\n(`1 foot → inch` would give `12.000000000000002`).\n\nInstead, each `Quantity` keeps its magnitude as an exact `Rational`, and\nconversions/arithmetic run in rational arithmetic, collapsing to a `number` only\nwhen you read `.magnitude` (or call `.in()`). Because nothing is collapsed\nmid-chain, conversions compose without accumulating drift:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { liter, usGallon } from \"measurable/dimensions\";\n\n// A round trip through an awkward ratio still lands exactly on 7.\nnew Quantity(7, liter).to(usGallon).to(liter).magnitude; // 7\n\n// .rational exposes the underlying exact value (always in lowest terms).\nnew Quantity(7, liter).to(usGallon).rational; // Rational 125000000/67596639\n```\n\nThis is exact for linear and affine units. A conversion that passes through a\nnon-linear `custom` unit (e.g. a logarithmic scale) necessarily uses floating\npoint and recaptures the result as a rational, so it is best-effort there.\n\n## Built-in dimensions\n\nImport any dimension or unit from `measurable/dimensions`:\n\n| Dimension | Base | Units (a selection) |\n| -------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `length` | `meter` | `kilometer`, `centimeter`, `millimeter`, `inch`, `foot`, `yard`, `mile` |\n| `area` | `squareMeter` | `squareKilometer`, `hectare`, `are`, `squareInch`, `squareFoot`, `squareYard`, `acre`, `squareMile` |\n| `volume` | `liter` | `milliliter`, `us*`/`imperial*` `Gallon`/`Quart`/`Pint`/`Gill`/`FluidOunce`, `cup`, `tablespoon`, `teaspoon` |\n| `mass` | `gram` | `kilogram`, `milligram`, `tonne`, `pound`, `ounce`, `stone`, `shortTon`, `longTon` |\n| `time` | `second` | `millisecond`, `minute`, `hour`, `day`, `week` |\n| `temperature` | `kelvin` | `celsius`, `fahrenheit` |\n| `angle` | `radian` | `degree`, `gradian`, `turn` |\n| `force` | `newton` | `kilonewton`, `dyne`, `poundForce`, `kilogramForce` |\n| `energy` | `joule` | `kilojoule`, `wattHour`, `kilowattHour`, `calorie`, `kilocalorie`, `britishThermalUnit` |\n| `power` | `watt` | `kilowatt`, `megawatt`, `horsepower`, `metricHorsepower` |\n| `pressure` | `pascal` | `kilopascal`, `bar`, `millibar`, `atmosphere`, `torr`, `psi`, `inchOfMercury`, `inchOfWater` |\n| `frequency` | `hertz` | `kilohertz`, `megahertz`, `gigahertz`, `terahertz` |\n| `data` | `bit` | `byte`, `nibble`, `kilobyte`…`petabyte` (SI), `kibibyte`…`pebibyte` (IEC) |\n| `illuminance` | `lux` | `kilolux`, `millilux`, `footCandle`, `phot` |\n| `luminance` | `candelaPerSquareMeter` | `nit`, `stilb` |\n| `luminousIntensity` | `candela` | `kilocandela`, `millicandela`, `candlepower`, `hefnerkerze` |\n\nThe metric units carry the **full SI prefix ladder** (yotta → yocto), generated for\nyou: the SI-based dimensions (`length`, `mass`, `volume`, `force`, `energy`, `power`,\n`pressure`, `frequency`, `illuminance`, `luminousIntensity`) get every prefix (so\n`kilogram` itself is just the kilo-prefixed gram), while `time` and `angle` get the\nfractional prefixes only (e.g. `millisecond`, `microradian`). Every generated rung\nparses from its symbol (`dm`, `hm`, `Mg`, `kL`, `ns`, `GHz`, `kPa`, and `µm`/`um` for\nmicro) and converts like any other unit. `data` additionally carries the **IEC binary**\nmultiples (`kibibyte`, `mebibyte`, … = 1024-based) alongside the SI decimal ones\n(`kilobyte`, … = 1000-based). You can apply the same ladder to your own dimensions with\n`definePrefixed` (see below).\n\n### Prefixed unit exports\n\nEach prefixed dimension exports a **record** holding the _complete_ generated ladder\nkeyed by name (the return value of `definePrefixed`), plus a **curated subset of those\nsame units as individual named exports** for convenience. Reach any rung that isn't\nexported individually through the record — e.g. `metricLength.gigameter`,\n`metricMass.exagram` — or by parsing its symbol.\n\n| Dimension | Record export(s) | Individually exported units |\n| ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |\n| `length` | `metricLength` | `kilometer`, `hectometer`, `decameter`, `decimeter`, `centimeter`, `millimeter`, `micrometer`, `nanometer` |\n| `mass` | `metricMass` | `kilogram`, `megagram`, `hectogram`, `decagram`, `decigram`, `centigram`, `milligram`, `microgram`, `nanogram` |\n| `volume` | `metricVolume` | `kiloliter`, `hectoliter`, `decaliter`, `deciliter`, `centiliter`, `milliliter` |\n| `time` | `metricTime` | `millisecond`, `microsecond`, `nanosecond`, `picosecond` |\n| `angle` | `metricAngle` | `milliradian`, `microradian` |\n| `force` | `metricForce` | `meganewton`, `kilonewton`, `millinewton`, `micronewton` |\n| `energy` | `metricEnergy`, `metricWattHour` | `kilojoule`, `megajoule`, `gigajoule`, `millijoule`; `kilowattHour`, `megawattHour`, `gigawattHour` |\n| `power` | `metricPower` | `kilowatt`, `megawatt`, `gigawatt`, `terawatt`, `milliwatt` |\n| `pressure` | `metricPressure` | `kilopascal`, `hectopascal`, `megapascal`, `gigapascal` |\n| `frequency` | `metricFrequency` | `kilohertz`, `megahertz`, `gigahertz`, `terahertz`, `petahertz`, `millihertz` |\n| `illuminance` | `metricIlluminance` | `kilolux`, `millilux`, `microlux` |\n| `luminousIntensity` | `metricLuminousIntensity` | `kilocandela`, `millicandela`, `microcandela` |\n| `data` | `dataMultiples` | `kilobit`/`kilobyte` … `petabit`/`petabyte` (SI), `kibibit`/`kibibyte` … `pebibit`/`pebibyte` (IEC) |\n\n```ts\nimport { metricLength, kilometer } from \"measurable/dimensions\";\n\nkilometer === metricLength.kilometer; // true — the named export is the same unit\nmetricLength.gigameter; // a rung not exported by name, reached via the record\n```\n\n## Built-in measurement systems\n\nImport `metric`, `imperial`, or `usCustomary` from `measurable/systems`.\n\n```ts\nimport { foot } from \"measurable/dimensions\";\nimport { imperial, usCustomary, metric } from \"measurable/systems\";\n\nimperial.has(foot); // true — a unit can belong to several systems\nusCustomary.has(foot); // true\nmetric.has(foot); // false\n```\n\nMembership spans every dimension: SI units (`squareMeter`, `pascal`, `watt`, `joule`,\n`hertz`, `lux`, `candela`, and their prefix ladders) are tagged into `metric`, while the\ncustomary ones (`squareFoot`, `acre`, `psi`, `horsepower`, `britishThermalUnit`,\n`footCandle`, `candlepower`) belong to both `imperial` and `usCustomary`. Units tied to\nno real-world standard (e.g. `atmosphere`, `torr`, and the `data` multiples) stay\nuntagged — they still convert, they just won't appear under any system.\n\n### Listing units in a system\n\n```ts\nimport { length } from \"measurable/dimensions\";\nimport { metric } from \"measurable/systems\";\n\nmetric.in(length).map((u) => u.name); // [\"meter\", \"kilometer\", \"centimeter\", \"millimeter\"]\n```\n\n### Best-fit formatting\n\n`express` re-expresses a quantity in a system's most readable unit (the largest\nunit whose magnitude is still ≥ 1):\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { meter } from \"measurable/dimensions\";\nimport { metric, imperial } from \"measurable/systems\";\n\nmetric.express(new Quantity(5000, meter)); // Quantity(5, kilometer)\nimperial.express(new Quantity(5000, meter)); // Quantity(3.107…, mile)\n```\n\nUnder the hood, `express` just hands the system's units for that dimension to\n`Quantity.best`, the same best-fit primitive you can call directly with whatever\nunits you like — handy when you want a custom shortlist rather than a whole\nsystem. `best` picks the largest unit whose absolute magnitude is still ≥ 1\n(falling back to the smallest when even that rounds below 1), so candidate order\ndoesn't matter:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { meter, kilometer, mile } from \"measurable/dimensions\";\n\nnew Quantity(5000, meter).best(meter, kilometer, mile); // Quantity(3.107…, mile)\nnew Quantity(1500, meter).best(meter, kilometer); // Quantity(1.5, kilometer)\nnew Quantity(500, meter).best(kilometer, mile); // Quantity(0.5, kilometer) — smallest fallback\n```\n\nIt requires at least one unit, and each must belong to the quantity's dimension\n(otherwise `DimensionMismatchError`).\n\n## Formatting output\n\nEach unit carries a canonical `symbol` (`\"g\"`, `\"km\"`, `\"°C\"`) and `plural`\n(`\"grams\"`, `\"kilometers\"`) alongside its `name`, so a `Quantity` can be rendered the\nway you want:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { gram } from \"measurable/dimensions\";\n\nnew Quantity(5, gram).toString(); // \"5 gram\" (always the bare name)\nnew Quantity(5, gram).format(); // \"5 grams\" (magnitude-aware)\nnew Quantity(1, gram).format(); // \"1 gram\" (singular at ±1)\nnew Quantity(5, gram).format({ unit: \"symbol\" }); // \"5 g\"\nnew Quantity(5, gram).format({ unit: \"name\" }); // \"5 gram\"\nnew Quantity(5, gram).format({ unit: \"plural\" }); // \"5 grams\"\n\n// Localize the magnitude with locale / numberFormat (passed to toLocaleString):\nnew Quantity(1234.5, meter).format({ locale: \"de-DE\" }); // \"1.234,5 meters\"\nnew Quantity(1.23456, meter).format({ numberFormat: { maximumFractionDigits: 2 } }); // \"1.23 meters\"\nnew Quantity(1234.5, kilometer).format({ locale: \"de-DE\", unit: \"symbol\" }); // \"1.234,5 km\"\n```\n\n`toString()` is intentionally stable (`\"<magnitude> <name>\"`). `format(options?)` is the\nflexible one: `unit` defaults to `\"auto\"` (singular `name` at ±1, otherwise `plural`) and\naccepts `\"name\"`, `\"plural\"`, or `\"symbol\"`. When a unit has no `symbol`/`plural`, those\nmodes fall back to its `name`.\n\nThe magnitude is rendered with `Number.prototype.toLocaleString`. Pass `locale` (a BCP 47\nlocale or array) and/or `numberFormat` (`Intl.NumberFormatOptions` — precision via\n`maximumFractionDigits`, grouping, `style`, …) to control it; with neither set, the\nruntime's default locale is used. Use `round(decimals)` to trim the magnitude first\n(`new Quantity(1.6213, mile).round(2)` → `1.62 mile`).\n\nWhen a single string won't do — e.g. styling the magnitude in a React component — use\n`formatParts(options?)`, which takes the same options but returns the rendered\n`{ magnitude, unit }` as separate strings for you to assemble:\n\n```tsx\nconst { magnitude, unit } = new Quantity(1234.5, kilometer).formatParts({ locale: \"de-DE\" });\n// { magnitude: \"1.234,5\", unit: \"kilometers\" }\nreturn <><b>{magnitude}</b> {unit}</>;\n```\n\n### Internationalization\n\n`format()` localizes the **magnitude** (via `locale`/`numberFormat`), but the **label** it\nappends — `symbol`/`plural` — is **English/canonical** convenience data, not a localization\nsystem: a single plural string can't model languages with several plural forms, and the\nnames themselves are English. For a fully localized label, delegate to `Intl.NumberFormat`,\nwhose `style: \"unit\"` localizes **and** pluralizes a curated set of units for you:\n\n```ts\nconst q = new Quantity(5, kilometer);\nnew Intl.NumberFormat(\"de\", { style: \"unit\", unit: \"kilometer\" }).format(q.magnitude);\n// \"5 Kilometer\"\nnew Intl.NumberFormat(\"fr\", { style: \"unit\", unit: \"kilometer\", unitDisplay: \"short\" })\n .format(q.magnitude); // \"5 km\"\n```\n\n## Parsing strings\n\n`Quantity.parse(input, dimension, options?)` reads a string into a `Quantity`.\nCompound inputs are summed and returned in the finest unit present:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { length, time } from \"measurable/dimensions\";\n\nQuantity.parse(\"1km\", length); // Quantity(1, kilometer)\nQuantity.parse(\"5 hr\", time); // Quantity(5, hour)\nQuantity.parse(\"5hr 20min\", time); // Quantity(320, minute)\n```\n\n### Ambiguous aliases\n\nSome names mean different things in different systems — a US gallon (3.785 L) is\nnot an imperial gallon (4.546 L), and `ton` could be short or long. These are\ndistinct units that share an alias, so an unqualified parse throws; pass a\n`prefer`red system to disambiguate:\n\n```ts\nimport { Quantity, AmbiguousUnitError } from \"measurable\";\nimport { volume, mass } from \"measurable/dimensions\";\nimport { usCustomary, imperial } from \"measurable/systems\";\n\nQuantity.parse(\"1 gallon\", volume); // throws AmbiguousUnitError\nQuantity.parse(\"1 gallon\", volume, { prefer: usCustomary }).unit.name; // \"usGallon\"\nQuantity.parse(\"1 ton\", mass, { prefer: imperial }).unit.name; // \"longTon\"\n```\n\nConversion itself is governed only by the dimension, so cross-system conversions\nalways work regardless of tags:\n\n```ts\nimport { shortTon, tonne } from \"measurable/dimensions\";\n\nnew Quantity(1, shortTon).in(tonne); // 0.90718474\n```\n\n## Arithmetic\n\nQuantities can be combined. `plus`/`minus` take another `Quantity` (converted into\nthe receiver's unit first, so the operands may use different units of the same\ndimension); `times`/`dividedBy` apply a dimensionless scalar (a `number` or a\n`Rational`), and `negate`/`abs` transform the magnitude. All return a **new**\n`Quantity` in the receiver's unit and leave the operands untouched. Like\nconversions, the arithmetic is exact — `q.times(3).dividedBy(3)` returns `q`.\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { kilometer, mile } from \"measurable/dimensions\";\n\nnew Quantity(1, mile).plus(new Quantity(1, kilometer)); // Quantity(1.6213…, mile)\nnew Quantity(1, mile).minus(new Quantity(1, kilometer)); // Quantity(0.3786…, mile)\nnew Quantity(2, mile).times(3); // Quantity(6, mile)\nnew Quantity(6, mile).dividedBy(2); // Quantity(3, mile)\n```\n\nShort aliases are available: **`add`** (`plus`), **`sub`** (`minus`), **`mul`**\n(`times`), **`div`** (`dividedBy`).\n\nCombining different dimensions throws `DimensionMismatchError`. Note that adding\n**affine** units (e.g. temperatures) is mathematically defined but physically\nquestionable, since it adds absolute points rather than a difference.\n\n## Ratios\n\n`ratioTo` divides two quantities of the **same dimension** and returns a plain\n(dimensionless) number — _how many of one fit in the other_:\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { liter, milliliter } from \"measurable/dimensions\";\n\n// How many 250 mL servings are in a 2 L bottle?\nnew Quantity(2, liter).ratioTo(new Quantity(250, milliliter)); // 8\n```\n\nThis is different from `.in(unit)`: `.in(milliliter)` only uses the *unit* on the\nright (giving `2000`), whereas `ratioTo` also uses the other quantity's\n**magnitude** (the `250`), so it answers \"how many of *that quantity* fit in this\none.\" It's the inverse of scalar `times` — `b.times(a.ratioTo(b))` reconstructs\n`a`. Comparing different dimensions throws `DimensionMismatchError`.\n\n## Comparison\n\n`equals`/`notEquals`/`lessThan`/`greaterThan`/`lessThanOrEqual`/`greaterThanOrEqual`\ncompare two quantities (the other is converted into the receiver's unit first),\nreturning a boolean. Comparing different dimensions throws `DimensionMismatchError`.\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { kilometer, meter } from \"measurable/dimensions\";\n\nnew Quantity(1, kilometer).equals(new Quantity(1000, meter)); // true\nnew Quantity(1, meter).lessThan(new Quantity(1, kilometer)); // true\nnew Quantity(1, kilometer).greaterThan(new Quantity(1, meter)); // true\n```\n\nShort aliases: **`eq`** (`equals`), **`ne`** (`notEquals`), **`lt`** (`lessThan`),\n**`gt`** (`greaterThan`), **`lte`** (`lessThanOrEqual`), **`gte`**\n(`greaterThanOrEqual`). Comparison is exact rational comparison, so quantities\nthat are mathematically equal compare equal even when reaching them involved a\nconversion that would have drifted in floating point — e.g.\n`new Quantity(7, liter).to(usGallon).to(liter).equals(new Quantity(7, liter))` is\n`true`.\n\n`compareTo(other)` returns `-1`, `0`, or `1`, suitable as an `Array#sort`\ncomparator: `quantities.sort((a, b) => a.compareTo(b))`.\n\n## Combining quantities\n\n`Quantity.min`/`max`/`sum` aggregate several quantities at once; `clamp` is an\ninstance method that bounds one quantity to a range. Each converts operands as\nneeded, so mixing dimensions throws `DimensionMismatchError`.\n\n```ts\nimport { Quantity } from \"measurable\";\nimport { kilometer, meter } from \"measurable/dimensions\";\n\nconst a = new Quantity(1, kilometer);\nconst b = new Quantity(500, meter);\n\nQuantity.min(a, b); // Quantity(500, meter) — the smaller\nQuantity.max(a, b); // Quantity(1, kilometer) — the larger\nQuantity.sum(a, b); // Quantity(1.5, kilometer) — total, in a's unit\nb.clamp(a, new Quantity(2, kilometer)); // b bounded to [a, 2 km], in b's unit\n```\n\n## Defining your own units\n\nCreate a `Dimension` and add units through its builder methods. `scale` is how\nmany base units make up one of the unit being defined. The optional final argument\nis a definition object — `{ symbol?, plural?, aliases? }` — whose `symbol` and\n`plural` feed `format()` and, like `aliases`, are also registered for parsing.\n\n```ts\nimport { Dimension, Quantity } from \"measurable\";\n\nconst data = new Dimension(\"data\");\nconst byte = data.base(\"byte\", { symbol: \"B\", plural: \"bytes\" }); // base unit (identity)\nconst kilobyte = data.unit(\"kilobyte\", 1024, { symbol: \"KB\", plural: \"kilobytes\" });\nconst megabyte = data.unit(\"megabyte\", 1024 ** 2, { symbol: \"MB\", plural: \"megabytes\" });\n\nnew Quantity(2, megabyte).in(kilobyte); // 2048\nnew Quantity(2, megabyte).format(); // \"2 megabytes\"\nnew Quantity(2, megabyte).format({ unit: \"symbol\" }); // \"2 MB\"\n```\n\nA numeric `scale` is read as the exact decimal you wrote (`0.0254` → `254/10000`),\nwhich is exact for any terminating decimal. For a ratio a decimal **can't**\nrepresent exactly — e.g. `5/9` — pass a `Rational` so it stays exact:\n\n```ts\nimport { Dimension, Rational } from \"measurable\";\n\nconst ratio = new Dimension(\"ratio\");\nratio.base(\"whole\");\nconst third = ratio.unit(\"third\", new Rational(1, 3)); // exact, not 0.3333…\n```\n\n### Affine units (offset, not just scale)\n\n```ts\nimport { Dimension, Rational } from \"measurable\";\n\nconst temperature = new Dimension(\"temperature\");\nconst kelvin = temperature.base(\"kelvin\", { symbol: \"K\" });\n// value_in_base = value * scale + offset\nconst celsius = temperature.affine(\"celsius\", { scale: 1, offset: 273.15 }, {\n symbol: \"°C\",\n aliases: [\"C\"],\n});\n// Fahrenheit's 5/9 isn't a terminating decimal — give it (and the derived\n// offset) as exact Rationals so conversions round-trip without drift.\nconst scale = new Rational(5, 9);\nconst fahrenheit = temperature.affine(\n \"fahrenheit\",\n { scale, offset: Rational.from(273.15).minus(new Rational(32).times(scale)) },\n { symbol: \"°F\", aliases: [\"F\"] },\n);\n```\n\n### Fully custom transforms\n\nFor anything non-linear, provide an explicit inverse pair:\n\n```ts\nconst dim = new Dimension(\"custom\");\ndim.base(\"base\");\ndim.custom(\"squared\", {\n toBase: (x) => x * x,\n fromBase: (x) => Math.sqrt(x),\n});\n```\n\n### Generating SI prefixes\n\n`definePrefixed` adds the metric prefix ladder to a reference **unit** and returns the\ncreated units keyed by name (skipping any name that already exists). It reads the\nreference's `name`, `symbol`, and scale straight off the unit (via `scaleOf`), so each\ngenerated unit gets a derived symbol (`b` → `kb`) and plural too — even when the\nreference isn't the base unit. Pass `SI_SUBMULTIPLE_PREFIXES` to generate fractions only.\n\n```ts\nimport { Dimension, Quantity, definePrefixed } from \"measurable\";\n\nconst data = new Dimension(\"data\");\nconst bit = data.base(\"bit\", { symbol: \"b\", plural: \"bits\" });\nconst prefixed = definePrefixed(data, bit);\n\nnew Quantity(1, prefixed.kilobit).in(bit); // 1000 (SI kilo = 1e3)\nprefixed.kilobit.symbol; // \"kb\"\nnew Quantity(5, prefixed.kilobit).format({ unit: \"symbol\" }); // \"5 kb\"\n```\n\n### Tagging units into a measurement system\n\n```ts\nimport { MeasurementSystem } from \"measurable\";\n\nconst si = new MeasurementSystem(\"si\").add(byte, kilobyte, megabyte);\nsi.has(kilobyte); // true\n```\n\n## API reference\n\n### `Dimension`\n\n- `new Dimension(name)`\n- `.base(name, def?)` — define the canonical base unit\n- `.unit(name, scale, def?)` — linear unit (`scale` base units per unit; `number | Rational`)\n- `.affine(name, { scale, offset }, def?)` — linear with additive offset (each `number | Rational`)\n- `.custom(name, { toBase, fromBase }, def?)` — arbitrary inverse pair, for non-linear units\n- `.convert(value, from, to)` — convert a raw `number` between two of its units\n- `.convertRational(value, from, to)` → `Rational` — exact conversion between two of its units\n- `.get(token)` — units matching a name/alias (`Unit[] | undefined`)\n- `.has(unit)`, `.units`, `.baseUnit`\n\n`def` is an optional `UnitDef`: `{ symbol?, plural?, aliases? }`. All three are\nregistered as parse tokens; `symbol`/`plural` are additionally stored on the `Unit`.\n\n### `Unit`\n\nA passive handle, normally created via a dimension's builder methods rather than\n`new Unit` directly. Read-only properties:\n\n- `.name` — the unit's canonical name\n- `.symbol?` — canonical symbol (e.g. `\"g\"`, `\"km\"`), if declared\n- `.plural?` — plural name (e.g. `\"grams\"`), if declared\n- `.dimension` — the `Dimension` it belongs to\n- `.linear` → `{ scale: Rational; offset: Rational } | undefined` — the exact transform for linear/affine units (`undefined` for `custom` ones)\n- `.toBase(value)` → `number` — convert a value in this unit to base units\n- `.fromBase(value)` → `number` — convert a value in base units to this unit\n\n### `Quantity`\n\n- `new Quantity(magnitude, unit)` — `magnitude` is a `number | Rational`; throws on a non-finite `number`\n- `.magnitude` → `number` — getter, derived from `.rational`\n- `.rational` → `Rational` — the exact magnitude (source of truth)\n- `.to(target)` → `Quantity`\n- `.in(target)` → `number`\n- `.toString()` → `string` — stable `\"<magnitude> <name>\"`, e.g. `\"5 kilometer\"`\n- `.format({ unit?, locale?, numberFormat? })` → `string` — `unit`: `\"auto\"` (default, magnitude-aware) / `\"name\"` / `\"plural\"` / `\"symbol\"`; `locale` + `numberFormat` localize the magnitude via `toLocaleString`\n- `.formatParts(options?)` → `{ magnitude, unit }` — same options as `.format`, but returns the rendered pieces separately for custom assembly (e.g. JSX)\n- `.plus(other)` / `.minus(other)` → `Quantity` — add/subtract another quantity (aliases: `add` / `sub`)\n- `.times(factor)` / `.dividedBy(divisor)` → `Quantity` — scale by a `number | Rational` (aliases: `mul` / `div`)\n- `.ratioTo(other)` → `number` — dimensionless ratio (how many of `other` fit in this)\n- `.negate()` / `.abs()` → `Quantity`\n- `.clamp(lower, upper)` → `Quantity` — bound to a range, in this unit\n- `.round(decimals?)` → `Quantity` — round the magnitude (default 0 decimals)\n- `.best(...units)` → `Quantity` — re-express in the largest given unit whose absolute magnitude is still ≥ 1 (smallest as fallback); needs ≥ 1 unit, all of this dimension\n- `.equals(other)` / `.notEquals(other)` → `boolean` (aliases: `eq` / `ne`)\n- `.lessThan(other)` / `.greaterThan(other)` → `boolean` (aliases: `lt` / `gt`)\n- `.lessThanOrEqual(other)` / `.greaterThanOrEqual(other)` → `boolean` (aliases: `lte` / `gte`)\n- `.compareTo(other)` → `-1 | 0 | 1` — sort comparator\n- `.isZero()` / `.isPositive()` / `.isNegative()` → `boolean`\n- `Quantity.min(...quantities)` / `Quantity.max(...quantities)` / `Quantity.sum(...quantities)` → `Quantity`\n- `Quantity.parse(input, dimension, { prefer? })` → `Quantity`\n\n### `Rational`\n\nAn exact rational number (`n / d`), stored as `bigint`s in lowest terms with a\npositive denominator. Immutable; every operation returns a new `Rational`. Used\ninternally for lossless conversions and arithmetic, but you can construct one to\npass anywhere a magnitude or scale is accepted.\n\n- `new Rational(numerator, denominator?)` — from integers (`bigint | number`; denominator defaults to `1`). Throws on a non-integer `number` or a zero denominator.\n- `Rational.from(value)` — coerce a `number | Rational` (a `number` is read as its exact terminating decimal, e.g. `0.0254` → `254/10000`)\n- `.n` / `.d` → `bigint` — numerator and denominator\n- `.plus(other)` / `.minus(other)` / `.times(other)` / `.dividedBy(other)` → `Rational` (aliases: `add` / `sub` / `mul` / `div`)\n- `.negate()` / `.abs()` → `Rational`\n- `.equals(other)` → `boolean` (alias: `eq`)\n- `.compare(other)` → `-1 | 0 | 1`\n- `.sign()` → `-1 | 0 | 1`\n- `.toNumber()` → `number` — collapse to the nearest `number`\n\n### `MeasurementSystem`\n\n- `new MeasurementSystem(name)`\n- `.add(...units)`, `.has(unit)`\n- `.in(dimension)` → `Unit[]`\n- `.express(quantity)` → `Quantity`\n\n### Errors\n\nAll extend the built-in `Error` (so existing `catch` blocks still catch them) and\nare exported from `measurable`, letting you branch on the failure with\n`instanceof`:\n\n- `DimensionMismatchError` — an operation was attempted on units from different dimensions (converting, comparing, or combining them). Exported as `InvalidConversionError` too, a deprecated alias of the same class.\n- `UnknownUnitError` — a parsed token matches no unit\n- `AmbiguousUnitError` — a parsed token matches several units and no `prefer` was given\n- `ParseError` — a string could not be parsed into a quantity at all\n- `DuplicateUnitError` — a unit name already exists in its dimension\n- `UnsupportedDimensionError` — a system has no units of a dimension to `express` in\n- `ArgumentError` — an argument was invalid (e.g. `best()` with no units, or a non-integer / zero-denominator / non-finite `Rational`)\n\n## Changelog\n\nSee [CHANGELOG.md](https://github.com/mhuggins/measurable/blob/main/CHANGELOG.md)\nfor release notes. Note that v2.0.0 is a breaking release — see its entry for the\nmigration details.\n\n## License\n\nISC\n"
63
63
  }