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 +35 -0
- package/README.md +33 -5
- package/dist/errors/AmbiguousUnitError.js +1 -2
- package/dist/errors/ArgumentError.d.ts +7 -0
- package/dist/errors/ArgumentError.js +11 -0
- package/dist/errors/DimensionMismatchError.d.ts +13 -0
- package/dist/errors/DimensionMismatchError.js +16 -0
- package/dist/errors/DuplicateUnitError.d.ts +5 -0
- package/dist/errors/DuplicateUnitError.js +10 -0
- package/dist/errors/ParseError.d.ts +4 -0
- package/dist/errors/ParseError.js +10 -0
- package/dist/errors/UnsupportedDimensionError.d.ts +10 -0
- package/dist/errors/UnsupportedDimensionError.js +14 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/lib/Dimension.js +4 -3
- package/dist/lib/MeasurementSystem.js +4 -15
- package/dist/lib/Quantity.d.ts +11 -3
- package/dist/lib/Quantity.js +29 -4
- package/dist/lib/Rational.js +5 -4
- package/package.json +2 -2
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
-
- `
|
|
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,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,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/
|
|
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/
|
|
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);
|
package/dist/lib/Dimension.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Dimension = void 0;
|
|
4
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
40
|
+
const candidates = this.in(quantity.unit.dimension);
|
|
41
41
|
if (candidates.length === 0) {
|
|
42
|
-
throw new
|
|
43
|
-
`"${quantity.unit.dimension.name}" units to express in`);
|
|
42
|
+
throw new UnsupportedDimensionError_1.UnsupportedDimensionError(this, quantity.unit.dimension);
|
|
44
43
|
}
|
|
45
|
-
|
|
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;
|
package/dist/lib/Quantity.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
package/dist/lib/Quantity.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
285
|
+
throw new ParseError_1.ParseError(str);
|
|
261
286
|
}
|
|
262
287
|
return total.to(finest);
|
|
263
288
|
}
|
package/dist/lib/Rational.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
}
|