measurable 2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +129 -27
  3. package/dist/dimensions/angle.js +12 -5
  4. package/dist/dimensions/area.js +37 -16
  5. package/dist/dimensions/data.js +11 -8
  6. package/dist/dimensions/energy.js +18 -11
  7. package/dist/dimensions/force.js +5 -5
  8. package/dist/dimensions/frequency.js +2 -2
  9. package/dist/dimensions/illuminance.js +7 -4
  10. package/dist/dimensions/length.js +6 -6
  11. package/dist/dimensions/luminance.js +5 -8
  12. package/dist/dimensions/luminousIntensity.js +7 -7
  13. package/dist/dimensions/mass.js +12 -8
  14. package/dist/dimensions/power.js +4 -4
  15. package/dist/dimensions/pressure.js +12 -9
  16. package/dist/dimensions/temperature.js +3 -3
  17. package/dist/dimensions/time.js +18 -6
  18. package/dist/dimensions/volume.js +56 -23
  19. package/dist/errors/AmbiguousUnitError.js +1 -2
  20. package/dist/errors/ArgumentError.d.ts +7 -0
  21. package/dist/errors/ArgumentError.js +11 -0
  22. package/dist/errors/DimensionMismatchError.d.ts +13 -0
  23. package/dist/errors/DimensionMismatchError.js +16 -0
  24. package/dist/errors/DuplicateUnitError.d.ts +5 -0
  25. package/dist/errors/DuplicateUnitError.js +10 -0
  26. package/dist/errors/ParseError.d.ts +4 -0
  27. package/dist/errors/ParseError.js +10 -0
  28. package/dist/errors/UnsupportedDimensionError.d.ts +10 -0
  29. package/dist/errors/UnsupportedDimensionError.js +14 -0
  30. package/dist/index.d.ts +5 -1
  31. package/dist/index.js +5 -1
  32. package/dist/lib/Dimension.d.ts +14 -5
  33. package/dist/lib/Dimension.js +22 -19
  34. package/dist/lib/MeasurementSystem.js +4 -15
  35. package/dist/lib/Quantity.d.ts +81 -3
  36. package/dist/lib/Quantity.js +82 -4
  37. package/dist/lib/Rational.d.ts +1 -1
  38. package/dist/lib/Rational.js +6 -5
  39. package/dist/lib/Unit.d.ts +18 -11
  40. package/dist/lib/Unit.js +7 -3
  41. package/dist/utils/definePrefixed.d.ts +8 -15
  42. package/dist/utils/definePrefixed.js +21 -10
  43. package/package.json +8 -3
package/CHANGELOG.md CHANGED
@@ -5,6 +5,88 @@ 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
+
42
+ ## [3.0.0] - 2026-06-18
43
+
44
+ This release adds value **formatting**. Units now carry a `symbol` and `plural`,
45
+ and a `Quantity` can render itself with magnitude-aware pluralization and
46
+ locale-aware number formatting. Threading symbol/plural through the unit builders
47
+ changes their signatures, hence the major bump.
48
+
49
+ ### Breaking
50
+
51
+ - **Unit builder methods take a `UnitDef` object instead of an aliases array.**
52
+ `Dimension.base` / `unit` / `affine` / `custom` now accept
53
+ `{ symbol?, plural?, aliases? }` as their final argument rather than a bare
54
+ `string[]` of aliases. Migrate `length.unit("inch", 0.0254, ["in", "inches"])`
55
+ to `length.unit("inch", 0.0254, { symbol: "in", plural: "inches" })`; plain
56
+ aliases still work via the `aliases` field.
57
+ - **`definePrefixed` takes the reference `Unit`, not a descriptor object.**
58
+ `definePrefixed(length, { name: "meter", symbol: "m" })` becomes
59
+ `definePrefixed(length, meter)` — it reads the name, symbol, and exact scale
60
+ straight off the unit. The `PrefixReference` type is removed.
61
+ - **`Unit`'s constructor options gained `symbol` / `plural`.** Only affects code
62
+ that calls `new Unit(...)` directly; units built through a `Dimension` are
63
+ unaffected.
64
+
65
+ ### Added
66
+
67
+ - **`Unit.symbol` and `Unit.plural`** — optional, first-class descriptors. Both
68
+ are also registered as parse tokens, so `Quantity.parse` accepts e.g. `"5 g"`
69
+ and `"5 grams"`.
70
+ - **`Quantity.format(options?)`** — renders `"<magnitude> <label>"`. `unit`
71
+ selects the label: `"auto"` (default — singular `name` at ±1, otherwise
72
+ `plural`), `"name"`, `"plural"`, or `"symbol"`, each falling back to `name`
73
+ when unset. The magnitude is rendered with `toLocaleString`; pass `locale`
74
+ and/or `numberFormat` (`Intl.NumberFormatOptions`) for locale- and
75
+ precision-aware output.
76
+ - **`Quantity.formatParts(options?)`** — the same options as `format`, but
77
+ returns the rendered `{ magnitude, unit }` as separate strings for custom
78
+ assembly (e.g. JSX).
79
+ - **`definePrefixed` derives each variant's `symbol` and `plural`** from the
80
+ reference unit (e.g. `meter` → `kilometer` / `km`), keeping the prefixed scale
81
+ exact via rational arithmetic.
82
+ - New exported types: `UnitDef`, `FormatOptions`, `FormattedParts`,
83
+ `BaseUnitOptions`, `UnitConversionOptions`, and `UnitOptions`.
84
+
85
+ ### Changed
86
+
87
+ - Added `assert-never` as a (small) runtime dependency, used for exhaustiveness
88
+ checking in formatting.
89
+
8
90
  ## [2.0.0] - 2026-06-18
9
91
 
10
92
  This release makes conversions and quantity arithmetic **exact**. Magnitudes and
@@ -94,6 +176,8 @@ to a `number` at the edge, so `foot → inch` is exactly `12` (not
94
176
  `MeasurementSystem`), string parsing via `Quantity.parse`, and the first set
95
177
  of built-in dimensions and measurement systems.
96
178
 
179
+ [3.1.0]: https://github.com/mhuggins/measurable/compare/v3.0.0...v3.1.0
180
+ [3.0.0]: https://github.com/mhuggins/measurable/compare/v2.0.0...v3.0.0
97
181
  [2.0.0]: https://github.com/mhuggins/measurable/compare/v1.1.1...v2.0.0
98
182
  [1.1.1]: https://github.com/mhuggins/measurable/compare/v1.1.0...v1.1.1
99
183
  [1.1.0]: https://github.com/mhuggins/measurable/compare/v1.0.0...v1.1.0
package/README.md CHANGED
@@ -206,9 +206,84 @@ 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
- A `Quantity` also has a `toString()` that renders `"<magnitude> <unit name>"`
210
- (e.g. `new Quantity(5, kilometer).toString()` `"5 kilometer"`), and `round(decimals)`
211
- to trim the magnitude for display (`new Quantity(1.6213, mile).round(2)` `1.62 mile`).
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
+
228
+ ## Formatting output
229
+
230
+ Each unit carries a canonical `symbol` (`"g"`, `"km"`, `"°C"`) and `plural`
231
+ (`"grams"`, `"kilometers"`) alongside its `name`, so a `Quantity` can be rendered the
232
+ way you want:
233
+
234
+ ```ts
235
+ import { Quantity } from "measurable";
236
+ import { gram } from "measurable/dimensions";
237
+
238
+ new Quantity(5, gram).toString(); // "5 gram" (always the bare name)
239
+ new Quantity(5, gram).format(); // "5 grams" (magnitude-aware)
240
+ new Quantity(1, gram).format(); // "1 gram" (singular at ±1)
241
+ new Quantity(5, gram).format({ unit: "symbol" }); // "5 g"
242
+ new Quantity(5, gram).format({ unit: "name" }); // "5 gram"
243
+ new Quantity(5, gram).format({ unit: "plural" }); // "5 grams"
244
+
245
+ // Localize the magnitude with locale / numberFormat (passed to toLocaleString):
246
+ new Quantity(1234.5, meter).format({ locale: "de-DE" }); // "1.234,5 meters"
247
+ new Quantity(1.23456, meter).format({ numberFormat: { maximumFractionDigits: 2 } }); // "1.23 meters"
248
+ new Quantity(1234.5, kilometer).format({ locale: "de-DE", unit: "symbol" }); // "1.234,5 km"
249
+ ```
250
+
251
+ `toString()` is intentionally stable (`"<magnitude> <name>"`). `format(options?)` is the
252
+ flexible one: `unit` defaults to `"auto"` (singular `name` at ±1, otherwise `plural`) and
253
+ accepts `"name"`, `"plural"`, or `"symbol"`. When a unit has no `symbol`/`plural`, those
254
+ modes fall back to its `name`.
255
+
256
+ The magnitude is rendered with `Number.prototype.toLocaleString`. Pass `locale` (a BCP 47
257
+ locale or array) and/or `numberFormat` (`Intl.NumberFormatOptions` — precision via
258
+ `maximumFractionDigits`, grouping, `style`, …) to control it; with neither set, the
259
+ runtime's default locale is used. Use `round(decimals)` to trim the magnitude first
260
+ (`new Quantity(1.6213, mile).round(2)` → `1.62 mile`).
261
+
262
+ When a single string won't do — e.g. styling the magnitude in a React component — use
263
+ `formatParts(options?)`, which takes the same options but returns the rendered
264
+ `{ magnitude, unit }` as separate strings for you to assemble:
265
+
266
+ ```tsx
267
+ const { magnitude, unit } = new Quantity(1234.5, kilometer).formatParts({ locale: "de-DE" });
268
+ // { magnitude: "1.234,5", unit: "kilometers" }
269
+ return <><b>{magnitude}</b> {unit}</>;
270
+ ```
271
+
272
+ ### Internationalization
273
+
274
+ `format()` localizes the **magnitude** (via `locale`/`numberFormat`), but the **label** it
275
+ appends — `symbol`/`plural` — is **English/canonical** convenience data, not a localization
276
+ system: a single plural string can't model languages with several plural forms, and the
277
+ names themselves are English. For a fully localized label, delegate to `Intl.NumberFormat`,
278
+ whose `style: "unit"` localizes **and** pluralizes a curated set of units for you:
279
+
280
+ ```ts
281
+ const q = new Quantity(5, kilometer);
282
+ new Intl.NumberFormat("de", { style: "unit", unit: "kilometer" }).format(q.magnitude);
283
+ // "5 Kilometer"
284
+ new Intl.NumberFormat("fr", { style: "unit", unit: "kilometer", unitDisplay: "short" })
285
+ .format(q.magnitude); // "5 km"
286
+ ```
212
287
 
213
288
  ## Parsing strings
214
289
 
@@ -272,7 +347,7 @@ new Quantity(6, mile).dividedBy(2); // Quantity(3, mile)
272
347
  Short aliases are available: **`add`** (`plus`), **`sub`** (`minus`), **`mul`**
273
348
  (`times`), **`div`** (`dividedBy`).
274
349
 
275
- Combining different dimensions throws `InvalidConversionError`. Note that adding
350
+ Combining different dimensions throws `DimensionMismatchError`. Note that adding
276
351
  **affine** units (e.g. temperatures) is mathematically defined but physically
277
352
  questionable, since it adds absolute points rather than a difference.
278
353
 
@@ -293,13 +368,13 @@ This is different from `.in(unit)`: `.in(milliliter)` only uses the *unit* on th
293
368
  right (giving `2000`), whereas `ratioTo` also uses the other quantity's
294
369
  **magnitude** (the `250`), so it answers "how many of *that quantity* fit in this
295
370
  one." It's the inverse of scalar `times` — `b.times(a.ratioTo(b))` reconstructs
296
- `a`. Comparing different dimensions throws `InvalidConversionError`.
371
+ `a`. Comparing different dimensions throws `DimensionMismatchError`.
297
372
 
298
373
  ## Comparison
299
374
 
300
375
  `equals`/`notEquals`/`lessThan`/`greaterThan`/`lessThanOrEqual`/`greaterThanOrEqual`
301
376
  compare two quantities (the other is converted into the receiver's unit first),
302
- returning a boolean. Comparing different dimensions throws `InvalidConversionError`.
377
+ returning a boolean. Comparing different dimensions throws `DimensionMismatchError`.
303
378
 
304
379
  ```ts
305
380
  import { Quantity } from "measurable";
@@ -325,7 +400,7 @@ comparator: `quantities.sort((a, b) => a.compareTo(b))`.
325
400
 
326
401
  `Quantity.min`/`max`/`sum` aggregate several quantities at once; `clamp` is an
327
402
  instance method that bounds one quantity to a range. Each converts operands as
328
- needed, so mixing dimensions throws `InvalidConversionError`.
403
+ needed, so mixing dimensions throws `DimensionMismatchError`.
329
404
 
330
405
  ```ts
331
406
  import { Quantity } from "measurable";
@@ -343,17 +418,21 @@ b.clamp(a, new Quantity(2, kilometer)); // b bounded to [a, 2 km], in b's unit
343
418
  ## Defining your own units
344
419
 
345
420
  Create a `Dimension` and add units through its builder methods. `scale` is how
346
- many base units make up one of the unit being defined.
421
+ many base units make up one of the unit being defined. The optional final argument
422
+ is a definition object — `{ symbol?, plural?, aliases? }` — whose `symbol` and
423
+ `plural` feed `format()` and, like `aliases`, are also registered for parsing.
347
424
 
348
425
  ```ts
349
426
  import { Dimension, Quantity } from "measurable";
350
427
 
351
428
  const data = new Dimension("data");
352
- const byte = data.base("byte", ["B", "bytes"]); // the base unit (identity)
353
- const kilobyte = data.unit("kilobyte", 1024, ["KB"]);
354
- const megabyte = data.unit("megabyte", 1024 ** 2, ["MB"]);
429
+ const byte = data.base("byte", { symbol: "B", plural: "bytes" }); // base unit (identity)
430
+ const kilobyte = data.unit("kilobyte", 1024, { symbol: "KB", plural: "kilobytes" });
431
+ const megabyte = data.unit("megabyte", 1024 ** 2, { symbol: "MB", plural: "megabytes" });
355
432
 
356
- new Quantity(2, megabyte).in(kilobyte); // 2048
433
+ new Quantity(2, megabyte).in(kilobyte); // 2048
434
+ new Quantity(2, megabyte).format(); // "2 megabytes"
435
+ new Quantity(2, megabyte).format({ unit: "symbol" }); // "2 MB"
357
436
  ```
358
437
 
359
438
  A numeric `scale` is read as the exact decimal you wrote (`0.0254` → `254/10000`),
@@ -374,16 +453,19 @@ const third = ratio.unit("third", new Rational(1, 3)); // exact, not 0.3333…
374
453
  import { Dimension, Rational } from "measurable";
375
454
 
376
455
  const temperature = new Dimension("temperature");
377
- const kelvin = temperature.base("kelvin", ["K"]);
456
+ const kelvin = temperature.base("kelvin", { symbol: "K" });
378
457
  // value_in_base = value * scale + offset
379
- const celsius = temperature.affine("celsius", { scale: 1, offset: 273.15 }, ["C"]);
458
+ const celsius = temperature.affine("celsius", { scale: 1, offset: 273.15 }, {
459
+ symbol: "°C",
460
+ aliases: ["C"],
461
+ });
380
462
  // Fahrenheit's 5/9 isn't a terminating decimal — give it (and the derived
381
463
  // offset) as exact Rationals so conversions round-trip without drift.
382
464
  const scale = new Rational(5, 9);
383
465
  const fahrenheit = temperature.affine(
384
466
  "fahrenheit",
385
467
  { scale, offset: Rational.from(273.15).minus(new Rational(32).times(scale)) },
386
- ["F"],
468
+ { symbol: "°F", aliases: ["F"] },
387
469
  );
388
470
  ```
389
471
 
@@ -402,18 +484,22 @@ dim.custom("squared", {
402
484
 
403
485
  ### Generating SI prefixes
404
486
 
405
- `definePrefixed` adds the metric prefix ladder to a reference unit and returns the
406
- created units keyed by name (skipping any name that already exists). Pass
407
- `SI_SUBMULTIPLE_PREFIXES` to generate fractions only.
487
+ `definePrefixed` adds the metric prefix ladder to a reference **unit** and returns the
488
+ created units keyed by name (skipping any name that already exists). It reads the
489
+ reference's `name`, `symbol`, and scale straight off the unit (via `scaleOf`), so each
490
+ generated unit gets a derived symbol (`b` → `kb`) and plural too — even when the
491
+ reference isn't the base unit. Pass `SI_SUBMULTIPLE_PREFIXES` to generate fractions only.
408
492
 
409
493
  ```ts
410
494
  import { Dimension, Quantity, definePrefixed } from "measurable";
411
495
 
412
496
  const data = new Dimension("data");
413
- const bit = data.base("bit", ["b"]);
414
- const prefixed = definePrefixed(data, { name: "bit", symbol: "b", scale: 1 });
497
+ const bit = data.base("bit", { symbol: "b", plural: "bits" });
498
+ const prefixed = definePrefixed(data, bit);
415
499
 
416
- new Quantity(1, prefixed.kilobit).in(bit); // 1000 (SI kilo = 1e3)
500
+ new Quantity(1, prefixed.kilobit).in(bit); // 1000 (SI kilo = 1e3)
501
+ prefixed.kilobit.symbol; // "kb"
502
+ new Quantity(5, prefixed.kilobit).format({ unit: "symbol" }); // "5 kb"
417
503
  ```
418
504
 
419
505
  ### Tagging units into a measurement system
@@ -430,21 +516,26 @@ si.has(kilobyte); // true
430
516
  ### `Dimension`
431
517
 
432
518
  - `new Dimension(name)`
433
- - `.base(name, aliases?)` — define the canonical base unit
434
- - `.unit(name, scale, aliases?)` — linear unit (`scale` base units per unit; `number | Rational`)
435
- - `.affine(name, { scale, offset }, aliases?)` — linear with additive offset (each `number | Rational`)
436
- - `.custom(name, { toBase, fromBase }, aliases?)` — arbitrary inverse pair, for non-linear units
519
+ - `.base(name, def?)` — define the canonical base unit
520
+ - `.unit(name, scale, def?)` — linear unit (`scale` base units per unit; `number | Rational`)
521
+ - `.affine(name, { scale, offset }, def?)` — linear with additive offset (each `number | Rational`)
522
+ - `.custom(name, { toBase, fromBase }, def?)` — arbitrary inverse pair, for non-linear units
437
523
  - `.convert(value, from, to)` — convert a raw `number` between two of its units
438
524
  - `.convertRational(value, from, to)` → `Rational` — exact conversion between two of its units
439
525
  - `.get(token)` — units matching a name/alias (`Unit[] | undefined`)
440
526
  - `.has(unit)`, `.units`, `.baseUnit`
441
527
 
528
+ `def` is an optional `UnitDef`: `{ symbol?, plural?, aliases? }`. All three are
529
+ registered as parse tokens; `symbol`/`plural` are additionally stored on the `Unit`.
530
+
442
531
  ### `Unit`
443
532
 
444
533
  A passive handle, normally created via a dimension's builder methods rather than
445
534
  `new Unit` directly. Read-only properties:
446
535
 
447
536
  - `.name` — the unit's canonical name
537
+ - `.symbol?` — canonical symbol (e.g. `"g"`, `"km"`), if declared
538
+ - `.plural?` — plural name (e.g. `"grams"`), if declared
448
539
  - `.dimension` — the `Dimension` it belongs to
449
540
  - `.linear` → `{ scale: Rational; offset: Rational } | undefined` — the exact transform for linear/affine units (`undefined` for `custom` ones)
450
541
  - `.toBase(value)` → `number` — convert a value in this unit to base units
@@ -457,13 +548,16 @@ A passive handle, normally created via a dimension's builder methods rather than
457
548
  - `.rational` → `Rational` — the exact magnitude (source of truth)
458
549
  - `.to(target)` → `Quantity`
459
550
  - `.in(target)` → `number`
460
- - `.toString()` → `string` — e.g. `"5 kilometer"`
551
+ - `.toString()` → `string` — stable `"<magnitude> <name>"`, e.g. `"5 kilometer"`
552
+ - `.format({ unit?, locale?, numberFormat? })` → `string` — `unit`: `"auto"` (default, magnitude-aware) / `"name"` / `"plural"` / `"symbol"`; `locale` + `numberFormat` localize the magnitude via `toLocaleString`
553
+ - `.formatParts(options?)` → `{ magnitude, unit }` — same options as `.format`, but returns the rendered pieces separately for custom assembly (e.g. JSX)
461
554
  - `.plus(other)` / `.minus(other)` → `Quantity` — add/subtract another quantity (aliases: `add` / `sub`)
462
555
  - `.times(factor)` / `.dividedBy(divisor)` → `Quantity` — scale by a `number | Rational` (aliases: `mul` / `div`)
463
556
  - `.ratioTo(other)` → `number` — dimensionless ratio (how many of `other` fit in this)
464
557
  - `.negate()` / `.abs()` → `Quantity`
465
558
  - `.clamp(lower, upper)` → `Quantity` — bound to a range, in this unit
466
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
467
561
  - `.equals(other)` / `.notEquals(other)` → `boolean` (aliases: `eq` / `ne`)
468
562
  - `.lessThan(other)` / `.greaterThan(other)` → `boolean` (aliases: `lt` / `gt`)
469
563
  - `.lessThanOrEqual(other)` / `.greaterThanOrEqual(other)` → `boolean` (aliases: `lte` / `gte`)
@@ -498,9 +592,17 @@ pass anywhere a magnitude or scale is accepted.
498
592
 
499
593
  ### Errors
500
594
 
501
- - `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.
502
600
  - `UnknownUnitError` — a parsed token matches no unit
503
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`)
504
606
 
505
607
  ## Changelog
506
608
 
@@ -5,10 +5,17 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Plane angle. Base unit: radian. */
7
7
  exports.angle = new Dimension_1.Dimension("angle");
8
- exports.radian = exports.angle.base("radian", ["rad", "radians"]);
9
- exports.degree = exports.angle.unit("degree", Math.PI / 180, ["deg", "°", "degrees"]);
10
- exports.gradian = exports.angle.unit("gradian", Math.PI / 200, ["grad", "gradians"]);
11
- exports.turn = exports.angle.unit("turn", 2 * Math.PI, ["turns", "revolution", "revolutions"]);
8
+ exports.radian = exports.angle.base("radian", { symbol: "rad", plural: "radians" });
9
+ exports.degree = exports.angle.unit("degree", Math.PI / 180, {
10
+ symbol: "°",
11
+ plural: "degrees",
12
+ aliases: ["deg"],
13
+ });
14
+ exports.gradian = exports.angle.unit("gradian", Math.PI / 200, { symbol: "grad", plural: "gradians" });
15
+ exports.turn = exports.angle.unit("turn", 2 * Math.PI, {
16
+ plural: "turns",
17
+ aliases: ["revolution", "revolutions"],
18
+ });
12
19
  /** SI-submultiple radians (milliradian, microradian, …); larger angles use degree/turn. */
13
- exports.metricAngle = (0, definePrefixed_1.definePrefixed)(exports.angle, { name: "radian", symbol: "rad" }, definePrefixed_1.SI_SUBMULTIPLE_PREFIXES);
20
+ exports.metricAngle = (0, definePrefixed_1.definePrefixed)(exports.angle, exports.radian, definePrefixed_1.SI_SUBMULTIPLE_PREFIXES);
14
21
  exports.milliradian = exports.metricAngle.milliradian, exports.microradian = exports.metricAngle.microradian;
@@ -4,22 +4,43 @@ exports.squareMile = exports.acre = exports.squareYard = exports.squareFoot = ex
4
4
  const Dimension_1 = require("../lib/Dimension");
5
5
  /** Area. Base unit: square meter. */
6
6
  exports.area = new Dimension_1.Dimension("area");
7
- exports.squareMeter = exports.area.base("squareMeter", ["m²", "m2", "square meter", "square meters"]);
7
+ exports.squareMeter = exports.area.base("squareMeter", {
8
+ symbol: "m²",
9
+ plural: "square meters",
10
+ aliases: ["m2", "square meter"],
11
+ });
8
12
  // Metric multiples/submultiples. Area scales as the square of the length
9
13
  // prefix, so these can't be generated by the linear SI prefix helper.
10
- exports.squareKilometer = exports.area.unit("squareKilometer", 1e6, ["km²", "km2"]);
11
- exports.hectare = exports.area.unit("hectare", 1e4, ["ha", "hectares"]);
12
- exports.are = exports.area.unit("are", 1e2, ["ares"]);
13
- exports.squareCentimeter = exports.area.unit("squareCentimeter", 1e-4, ["cm²", "cm2"]);
14
- exports.squareMillimeter = exports.area.unit("squareMillimeter", 1e-6, ["mm²", "mm2"]);
14
+ exports.squareKilometer = exports.area.unit("squareKilometer", 1e6, {
15
+ symbol: "km²",
16
+ aliases: ["km2"],
17
+ });
18
+ exports.hectare = exports.area.unit("hectare", 1e4, { symbol: "ha", plural: "hectares" });
19
+ exports.are = exports.area.unit("are", 1e2, { plural: "ares" });
20
+ exports.squareCentimeter = exports.area.unit("squareCentimeter", 1e-4, {
21
+ symbol: "cm²",
22
+ aliases: ["cm2"],
23
+ });
24
+ exports.squareMillimeter = exports.area.unit("squareMillimeter", 1e-6, {
25
+ symbol: "mm²",
26
+ aliases: ["mm2"],
27
+ });
15
28
  // Imperial / US customary.
16
- exports.squareInch = exports.area.unit("squareInch", 6.4516e-4, ["sq in", "in²", "in2"]);
17
- exports.squareFoot = exports.area.unit("squareFoot", 9.290304e-2, [
18
- "sq ft",
19
- "ft²",
20
- "ft2",
21
- "square feet",
22
- ]);
23
- exports.squareYard = exports.area.unit("squareYard", 0.83612736, ["sq yd", "yd²", "yd2"]);
24
- exports.acre = exports.area.unit("acre", 4046.8564224, ["ac", "acres"]);
25
- exports.squareMile = exports.area.unit("squareMile", 2589988.110336, ["sq mi", "mi²", "mi2"]);
29
+ exports.squareInch = exports.area.unit("squareInch", 6.4516e-4, {
30
+ symbol: "in²",
31
+ aliases: ["sq in", "in2"],
32
+ });
33
+ exports.squareFoot = exports.area.unit("squareFoot", 9.290304e-2, {
34
+ symbol: "ft²",
35
+ plural: "square feet",
36
+ aliases: ["sq ft", "ft2"],
37
+ });
38
+ exports.squareYard = exports.area.unit("squareYard", 0.83612736, {
39
+ symbol: "yd²",
40
+ aliases: ["sq yd", "yd2"],
41
+ });
42
+ exports.acre = exports.area.unit("acre", 4046.8564224, { symbol: "ac", plural: "acres" });
43
+ exports.squareMile = exports.area.unit("squareMile", 2589988.110336, {
44
+ symbol: "mi²",
45
+ aliases: ["sq mi", "mi2"],
46
+ });
@@ -4,9 +4,9 @@ exports.pebibyte = exports.pebibit = exports.tebibyte = exports.tebibit = export
4
4
  const Dimension_1 = require("../lib/Dimension");
5
5
  /** Digital information. Base unit: bit. */
6
6
  exports.data = new Dimension_1.Dimension("data");
7
- exports.bit = exports.data.base("bit", ["b", "bits"]);
8
- exports.nibble = exports.data.unit("nibble", 4, ["nibbles"]);
9
- exports.byte = exports.data.unit("byte", 8, ["B", "bytes"]);
7
+ exports.bit = exports.data.base("bit", { symbol: "b", plural: "bits" });
8
+ exports.nibble = exports.data.unit("nibble", 4, { plural: "nibbles" });
9
+ exports.byte = exports.data.unit("byte", 8, { symbol: "B", plural: "bytes" });
10
10
  // SI (decimal, 1000-based) and IEC (binary, 1024-based) multiples of the bit
11
11
  // and byte. SI uses the bare symbol (kb, kB); IEC uses the "i" infix (Kib, KiB).
12
12
  const SI_MULTIPLES = [
@@ -25,11 +25,14 @@ const IEC_MULTIPLES = [
25
25
  ];
26
26
  const multiples = {};
27
27
  for (const [prefix, symbol, factor] of [...SI_MULTIPLES, ...IEC_MULTIPLES]) {
28
- multiples[`${prefix}bit`] = exports.data.unit(`${prefix}bit`, factor, [`${symbol}b`, `${prefix}bits`]);
29
- multiples[`${prefix}byte`] = exports.data.unit(`${prefix}byte`, 8 * factor, [
30
- `${symbol}B`,
31
- `${prefix}bytes`,
32
- ]);
28
+ multiples[`${prefix}bit`] = exports.data.unit(`${prefix}bit`, factor, {
29
+ symbol: `${symbol}b`,
30
+ plural: `${prefix}bits`,
31
+ });
32
+ multiples[`${prefix}byte`] = exports.data.unit(`${prefix}byte`, 8 * factor, {
33
+ symbol: `${symbol}B`,
34
+ plural: `${prefix}bytes`,
35
+ });
33
36
  }
34
37
  /** Every SI and IEC multiple of the bit and byte, keyed by name. */
35
38
  exports.dataMultiples = multiples;
@@ -5,18 +5,25 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Energy. Base unit: joule. */
7
7
  exports.energy = new Dimension_1.Dimension("energy");
8
- exports.joule = exports.energy.base("joule", ["J", "joules"]);
9
- exports.calorie = exports.energy.unit("calorie", 4.184, ["cal", "calories"]);
10
- exports.kilocalorie = exports.energy.unit("kilocalorie", 4184, ["kcal", "Cal", "kilocalories"]);
11
- exports.britishThermalUnit = exports.energy.unit("britishThermalUnit", 1055.05585262, ["BTU", "Btu"]);
12
- exports.wattHour = exports.energy.unit("wattHour", 3600, ["Wh", "watt-hour", "watt-hours"]);
8
+ exports.joule = exports.energy.base("joule", { symbol: "J", plural: "joules" });
9
+ exports.calorie = exports.energy.unit("calorie", 4.184, { symbol: "cal", plural: "calories" });
10
+ exports.kilocalorie = exports.energy.unit("kilocalorie", 4184, {
11
+ symbol: "kcal",
12
+ plural: "kilocalories",
13
+ aliases: ["Cal"],
14
+ });
15
+ exports.britishThermalUnit = exports.energy.unit("britishThermalUnit", 1055.05585262, {
16
+ symbol: "BTU",
17
+ aliases: ["Btu"],
18
+ });
19
+ exports.wattHour = exports.energy.unit("wattHour", 3600, {
20
+ symbol: "Wh",
21
+ plural: "watt-hours",
22
+ aliases: ["watt-hour"],
23
+ });
13
24
  /** Every SI-prefixed joule (kilojoule, megajoule, millijoule, …), keyed by name. */
14
- exports.metricEnergy = (0, definePrefixed_1.definePrefixed)(exports.energy, { name: "joule", symbol: "J" });
25
+ exports.metricEnergy = (0, definePrefixed_1.definePrefixed)(exports.energy, exports.joule);
15
26
  exports.kilojoule = exports.metricEnergy.kilojoule, exports.megajoule = exports.metricEnergy.megajoule, exports.gigajoule = exports.metricEnergy.gigajoule, exports.millijoule = exports.metricEnergy.millijoule;
16
27
  /** Every SI-prefixed watt-hour (kilowatt-hour, megawatt-hour, …), keyed by name. */
17
- exports.metricWattHour = (0, definePrefixed_1.definePrefixed)(exports.energy, {
18
- name: "wattHour",
19
- symbol: "Wh",
20
- scale: 3600,
21
- });
28
+ exports.metricWattHour = (0, definePrefixed_1.definePrefixed)(exports.energy, exports.wattHour);
22
29
  exports.kilowattHour = exports.metricWattHour.kilowattHour, exports.megawattHour = exports.metricWattHour.megawattHour, exports.gigawattHour = exports.metricWattHour.gigawattHour;
@@ -5,10 +5,10 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Force. Base unit: newton. */
7
7
  exports.force = new Dimension_1.Dimension("force");
8
- exports.newton = exports.force.base("newton", ["N", "newtons"]);
9
- exports.dyne = exports.force.unit("dyne", 0.00001, ["dyn", "dynes"]);
10
- exports.poundForce = exports.force.unit("poundForce", 4.4482216152605, ["lbf"]);
11
- exports.kilogramForce = exports.force.unit("kilogramForce", 9.80665, ["kgf"]);
8
+ exports.newton = exports.force.base("newton", { symbol: "N", plural: "newtons" });
9
+ exports.dyne = exports.force.unit("dyne", 0.00001, { symbol: "dyn", plural: "dynes" });
10
+ exports.poundForce = exports.force.unit("poundForce", 4.4482216152605, { symbol: "lbf" });
11
+ exports.kilogramForce = exports.force.unit("kilogramForce", 9.80665, { symbol: "kgf" });
12
12
  /** Every SI-prefixed newton (kilonewton, meganewton, millinewton, …), keyed by name. */
13
- exports.metricForce = (0, definePrefixed_1.definePrefixed)(exports.force, { name: "newton", symbol: "N" });
13
+ exports.metricForce = (0, definePrefixed_1.definePrefixed)(exports.force, exports.newton);
14
14
  exports.meganewton = exports.metricForce.meganewton, exports.kilonewton = exports.metricForce.kilonewton, exports.millinewton = exports.metricForce.millinewton, exports.micronewton = exports.metricForce.micronewton;
@@ -5,7 +5,7 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Frequency. Base unit: hertz. */
7
7
  exports.frequency = new Dimension_1.Dimension("frequency");
8
- exports.hertz = exports.frequency.base("hertz", ["Hz"]);
8
+ exports.hertz = exports.frequency.base("hertz", { symbol: "Hz" });
9
9
  /** Every SI-prefixed hertz (kilohertz, megahertz, gigahertz, …), keyed by name. */
10
- exports.metricFrequency = (0, definePrefixed_1.definePrefixed)(exports.frequency, { name: "hertz", symbol: "Hz" });
10
+ exports.metricFrequency = (0, definePrefixed_1.definePrefixed)(exports.frequency, exports.hertz);
11
11
  exports.kilohertz = exports.metricFrequency.kilohertz, exports.megahertz = exports.metricFrequency.megahertz, exports.gigahertz = exports.metricFrequency.gigahertz, exports.terahertz = exports.metricFrequency.terahertz, exports.petahertz = exports.metricFrequency.petahertz, exports.millihertz = exports.metricFrequency.millihertz;
@@ -5,9 +5,12 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Illuminance. Base unit: lux. */
7
7
  exports.illuminance = new Dimension_1.Dimension("illuminance");
8
- exports.lux = exports.illuminance.base("lux", ["lx"]);
9
- exports.footCandle = exports.illuminance.unit("footCandle", 10.764, ["fc", "ft-c"]);
10
- exports.phot = exports.illuminance.unit("phot", 10000, ["ph", "phots"]);
8
+ exports.lux = exports.illuminance.base("lux", { symbol: "lx" });
9
+ exports.footCandle = exports.illuminance.unit("footCandle", 10.764, {
10
+ symbol: "fc",
11
+ aliases: ["ft-c"],
12
+ });
13
+ exports.phot = exports.illuminance.unit("phot", 10000, { symbol: "ph", plural: "phots" });
11
14
  /** Every SI-prefixed lux (kilolux, millilux, microlux, …), keyed by name. */
12
- exports.metricIlluminance = (0, definePrefixed_1.definePrefixed)(exports.illuminance, { name: "lux", symbol: "lx" });
15
+ exports.metricIlluminance = (0, definePrefixed_1.definePrefixed)(exports.illuminance, exports.lux);
13
16
  exports.kilolux = exports.metricIlluminance.kilolux, exports.millilux = exports.metricIlluminance.millilux, exports.microlux = exports.metricIlluminance.microlux;
@@ -5,12 +5,12 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Length / distance. Base unit: meter. */
7
7
  exports.length = new Dimension_1.Dimension("length");
8
- exports.meter = exports.length.base("meter", ["m", "meters"]);
8
+ exports.meter = exports.length.base("meter", { symbol: "m", plural: "meters" });
9
9
  // Imperial / US customary.
10
- exports.inch = exports.length.unit("inch", 0.0254, ["in", "inches"]);
11
- exports.foot = exports.length.unit("foot", 0.3048, ["ft", "feet"]);
12
- exports.yard = exports.length.unit("yard", 0.9144, ["yd", "yards"]);
13
- exports.mile = exports.length.unit("mile", 1609.344, ["mi", "miles"]);
10
+ exports.inch = exports.length.unit("inch", 0.0254, { symbol: "in", plural: "inches" });
11
+ exports.foot = exports.length.unit("foot", 0.3048, { symbol: "ft", plural: "feet" });
12
+ exports.yard = exports.length.unit("yard", 0.9144, { symbol: "yd", plural: "yards" });
13
+ exports.mile = exports.length.unit("mile", 1609.344, { symbol: "mi", plural: "miles" });
14
14
  /** Every SI-prefixed meter (kilometer, centimeter, micrometer, …), keyed by name. */
15
- exports.metricLength = (0, definePrefixed_1.definePrefixed)(exports.length, { name: "meter", symbol: "m" });
15
+ exports.metricLength = (0, definePrefixed_1.definePrefixed)(exports.length, exports.meter);
16
16
  exports.kilometer = exports.metricLength.kilometer, exports.hectometer = exports.metricLength.hectometer, exports.decameter = exports.metricLength.decameter, exports.decimeter = exports.metricLength.decimeter, exports.centimeter = exports.metricLength.centimeter, exports.millimeter = exports.metricLength.millimeter, exports.micrometer = exports.metricLength.micrometer, exports.nanometer = exports.metricLength.nanometer;
@@ -4,13 +4,10 @@ exports.nit = exports.stilb = exports.candelaPerSquareMeter = exports.luminance
4
4
  const Dimension_1 = require("../lib/Dimension");
5
5
  /** Luminance. Base unit: candela per square meter (the nit). */
6
6
  exports.luminance = new Dimension_1.Dimension("luminance");
7
- exports.candelaPerSquareMeter = exports.luminance.base("candelaPerSquareMeter", [
8
- "cd/m²",
9
- "cd/m2",
10
- "nit",
11
- "nits",
12
- "nt",
13
- ]);
14
- exports.stilb = exports.luminance.unit("stilb", 1e4, ["sb"]);
7
+ exports.candelaPerSquareMeter = exports.luminance.base("candelaPerSquareMeter", {
8
+ symbol: "cd/m²",
9
+ aliases: ["cd/m2", "nit", "nits", "nt"],
10
+ });
11
+ exports.stilb = exports.luminance.unit("stilb", 1e4, { symbol: "sb" });
15
12
  /** Alias for {@link candelaPerSquareMeter}. */
16
13
  exports.nit = exports.candelaPerSquareMeter;
@@ -5,12 +5,12 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Luminous intensity. Base unit: candela. */
7
7
  exports.luminousIntensity = new Dimension_1.Dimension("luminousIntensity");
8
- exports.candela = exports.luminousIntensity.base("candela", ["cd"]);
9
- exports.candlepower = exports.luminousIntensity.unit("candlepower", 1, ["cp", "CP"]);
10
- exports.hefnerkerze = exports.luminousIntensity.unit("hefnerkerze", 0.92, ["HK"]);
11
- /** Every SI-prefixed candela (kilocandela, millicandela, …), keyed by name. */
12
- exports.metricLuminousIntensity = (0, definePrefixed_1.definePrefixed)(exports.luminousIntensity, {
13
- name: "candela",
14
- symbol: "cd",
8
+ exports.candela = exports.luminousIntensity.base("candela", { symbol: "cd", plural: "candelas" });
9
+ exports.candlepower = exports.luminousIntensity.unit("candlepower", 1, {
10
+ symbol: "cp",
11
+ aliases: ["CP"],
15
12
  });
13
+ exports.hefnerkerze = exports.luminousIntensity.unit("hefnerkerze", 0.92, { symbol: "HK" });
14
+ /** Every SI-prefixed candela (kilocandela, millicandela, …), keyed by name. */
15
+ exports.metricLuminousIntensity = (0, definePrefixed_1.definePrefixed)(exports.luminousIntensity, exports.candela);
16
16
  exports.kilocandela = exports.metricLuminousIntensity.kilocandela, exports.millicandela = exports.metricLuminousIntensity.millicandela, exports.microcandela = exports.metricLuminousIntensity.microcandela;