measurable 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +179 -35
  3. package/dist/dimensions/angle.js +2 -2
  4. package/dist/dimensions/area.d.ts +14 -0
  5. package/dist/dimensions/area.js +25 -0
  6. package/dist/dimensions/data.d.ts +10 -0
  7. package/dist/dimensions/data.js +36 -0
  8. package/dist/dimensions/energy.d.ts +14 -0
  9. package/dist/dimensions/energy.js +22 -0
  10. package/dist/dimensions/force.js +2 -2
  11. package/dist/dimensions/frequency.d.ts +7 -0
  12. package/dist/dimensions/frequency.js +11 -0
  13. package/dist/dimensions/illuminance.d.ts +9 -0
  14. package/dist/dimensions/illuminance.js +13 -0
  15. package/dist/dimensions/index.d.ts +9 -0
  16. package/dist/dimensions/index.js +9 -0
  17. package/dist/dimensions/length.js +2 -2
  18. package/dist/dimensions/luminance.d.ts +7 -0
  19. package/dist/dimensions/luminance.js +16 -0
  20. package/dist/dimensions/luminousIntensity.d.ts +9 -0
  21. package/dist/dimensions/luminousIntensity.js +16 -0
  22. package/dist/dimensions/mass.js +2 -2
  23. package/dist/dimensions/power.d.ts +9 -0
  24. package/dist/dimensions/power.js +13 -0
  25. package/dist/dimensions/pressure.d.ts +14 -0
  26. package/dist/dimensions/pressure.js +18 -0
  27. package/dist/dimensions/temperature.js +7 -1
  28. package/dist/dimensions/time.js +2 -2
  29. package/dist/dimensions/volume.js +2 -2
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +2 -1
  32. package/dist/lib/Dimension.d.ts +37 -11
  33. package/dist/lib/Dimension.js +49 -14
  34. package/dist/lib/MeasurementSystem.js +2 -2
  35. package/dist/lib/Quantity.d.ts +24 -10
  36. package/dist/lib/Quantity.js +39 -37
  37. package/dist/lib/Rational.d.ts +58 -0
  38. package/dist/lib/Rational.js +174 -0
  39. package/dist/lib/Unit.d.ts +41 -7
  40. package/dist/lib/Unit.js +35 -6
  41. package/dist/lib/prefixes.d.ts +2 -2
  42. package/dist/lib/prefixes.js +1 -1
  43. package/dist/systems/imperial.js +19 -1
  44. package/dist/systems/metric.js +25 -1
  45. package/dist/systems/usCustomary.js +19 -1
  46. package/dist/utils/definePrefixed.d.ts +35 -0
  47. package/dist/utils/definePrefixed.js +61 -0
  48. package/dist/utils/scaleOf.d.ts +3 -0
  49. package/dist/utils/scaleOf.js +6 -0
  50. package/package.json +8 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.0] - 2026-06-18
9
+
10
+ This release makes conversions and quantity arithmetic **exact**. Magnitudes and
11
+ unit transforms are represented as exact rationals internally and only collapse
12
+ to a `number` at the edge, so `foot → inch` is exactly `12` (not
13
+ `12.000000000000002`) and chains and round trips no longer accumulate drift.
14
+
15
+ ### Breaking
16
+
17
+ - **`Quantity.magnitude` is now a read-only getter** derived from the new
18
+ `Quantity.rational`, rather than a writable field. Assigning to
19
+ `quantity.magnitude` no longer works.
20
+ - **`Quantity` construction rejects non-finite magnitudes.**
21
+ `new Quantity(Infinity, unit)` (or `NaN`) now throws instead of being accepted.
22
+ - **`Unit.toBase` / `Unit.fromBase` are methods, not function-valued fields.**
23
+ Calling `unit.toBase(x)` is unchanged; holding an unbound reference such as
24
+ `const f = unit.toBase` no longer works.
25
+ - **`Unit`'s constructor options changed shape** to a discriminated union
26
+ (`{ linear }` vs `{ toBase, fromBase }`). Only affects code that calls
27
+ `new Unit(...)` directly; the `Dimension` builder methods are unaffected.
28
+ - **Conversion results changed value.** Outputs are now exact, so values that
29
+ previously carried floating-point error differ (e.g. `12.000000000000002` is
30
+ now `12`). `Quantity` equality and comparison are now exact rational
31
+ comparisons rather than float `===`, so quantities that are mathematically
32
+ equal compare equal even across a conversion that previously drifted.
33
+ - **Requires Node >= 14** (declared via `engines`); the library now uses `bigint`.
34
+
35
+ ### Added
36
+
37
+ - **`Rational`** — an exact rational number (`bigint` numerator/denominator),
38
+ exported from the package root. Supports `plus`/`minus`/`times`/`dividedBy`
39
+ (with `add`/`sub`/`mul`/`div` aliases), `negate`/`abs`, `equals`/`eq`,
40
+ `compare`, `sign`, `toNumber`, and the static `Rational.from`.
41
+ - **`Quantity.rational`** exposes the exact magnitude; the constructor and
42
+ `times`/`dividedBy` now accept `number | Rational`.
43
+ - **`Dimension.convertRational(value, from, to)`** — exact, rational-in /
44
+ rational-out conversion.
45
+ - **`Unit.linear`** — the exact `{ scale, offset }` transform for linear and
46
+ affine units (`undefined` for `custom` ones).
47
+ - **`Dimension.unit` and `Dimension.affine` accept `number | Rational`**, so a
48
+ ratio a decimal cannot represent exactly (e.g. Fahrenheit's `5/9`) can be
49
+ given exactly.
50
+ - New built-in dimensions: `area`, `data`, `energy`, `frequency`,
51
+ `illuminance`, `luminance`, `luminousIntensity`, `power`, and `pressure`
52
+ (each with its SI prefix ladder where applicable).
53
+ - `definePrefixed`'s reference `scale` is now optional, defaulting to `1`.
54
+
55
+ ### Fixed
56
+
57
+ - Linear and affine conversions are now exact (`foot → inch` is `12`, `1 L → mL`
58
+ round trips cleanly, and so on).
59
+ - SI-prefixed scales are computed in rational arithmetic, fixing drifted scales
60
+ such as `nanowattHour` (`3600 × 1e-9`).
61
+ - Built-in Fahrenheit is defined with exact rationals and round-trips without
62
+ drift in both directions.
63
+ - `Rational.toNumber` stays correctly rounded at extreme magnitudes (operands
64
+ beyond `2^53`), where converting each operand to a double before dividing
65
+ would otherwise lose precision.
66
+
67
+ ## [1.1.1] - 2026-06-17
68
+
69
+ ### Fixed
70
+
71
+ - Enable `embed-readme` so the README renders on npmjs.com.
72
+
73
+ ## [1.1.0] - 2026-06-17
74
+
75
+ ### Added
76
+
77
+ - SI metric prefix ladders generated across the metric dimensions.
78
+ - `Quantity` arithmetic — `plus` / `minus` / `times` / `dividedBy` (with
79
+ `add` / `sub` / `mul` / `div` aliases).
80
+ - `Quantity` comparison — `equals` / `notEquals` / `lessThan` / `greaterThan` /
81
+ `lessThanOrEqual` / `greaterThanOrEqual` (with `eq` / `ne` / `lt` / `gt` /
82
+ `lte` / `gte` aliases), plus `compareTo` as a sort comparator.
83
+ - `Quantity.ratioTo` for the dimensionless ratio between two quantities.
84
+ - `Quantity.abs` and the `Quantity.min` / `max` / `sum` statics.
85
+ - `clamp` as an instance method bounding a quantity to a range.
86
+ - `Quantity.toString`.
87
+ - `Quantity.isZero` / `isPositive` / `isNegative` predicates and `round`.
88
+
89
+ ## [1.0.0] - 2026-06-16
90
+
91
+ ### Added
92
+
93
+ - Initial release: the core conversion engine (`Dimension`, `Unit`, `Quantity`,
94
+ `MeasurementSystem`), string parsing via `Quantity.parse`, and the first set
95
+ of built-in dimensions and measurement systems.
96
+
97
+ [2.0.0]: https://github.com/mhuggins/measurable/compare/v1.1.1...v2.0.0
98
+ [1.1.1]: https://github.com/mhuggins/measurable/compare/v1.1.0...v1.1.1
99
+ [1.1.0]: https://github.com/mhuggins/measurable/compare/v1.0.0...v1.1.0
100
+ [1.0.0]: https://github.com/mhuggins/measurable/releases/tag/v1.0.0
package/README.md CHANGED
@@ -3,9 +3,14 @@
3
3
  Convert between units of measurement, with batteries-included common units and
4
4
  first-class support for defining your own.
5
5
 
6
- - **No drift** — each unit defines a single transform to its dimension's base
7
- unit; reverse conversions are derived, never stored, so they can't fall out of
8
- sync.
6
+ - **Exact, no drift** — magnitudes are held as exact rational numbers and
7
+ conversions run in rational arithmetic, collapsing to a float only when you
8
+ read `.magnitude`. So `foot → inch` is exactly `12` (not `12.000000000000002`),
9
+ and chains and round trips like `liter → gallon → liter` come back to exactly
10
+ what you started with.
11
+ - **No redundant factors** — each unit defines a single transform to its
12
+ dimension's base unit; reverse conversions are derived, never stored, so they
13
+ can't fall out of sync.
9
14
  - **Free chaining** — any unit converts to any other in the same dimension
10
15
  (e.g. `mile → inch`) without you defining every pair.
11
16
  - **Affine units** — temperature scales (°C/°F/K) and anything else needing an
@@ -26,7 +31,7 @@ The package is split into three import paths so the core stays lean:
26
31
 
27
32
  | Import | What you get |
28
33
  | -------------------------- | ------------------------------------------------------------------------- |
29
- | `measurable` | The building blocks: `Quantity`, `Dimension`, `MeasurementSystem`, `Unit`, errors |
34
+ | `measurable` | The building blocks: `Quantity`, `Dimension`, `MeasurementSystem`, `Unit`, `Rational`, errors |
30
35
  | `measurable/dimensions` | Predefined dimensions and their units (`length`, `meter`, `volume`, …) |
31
36
  | `measurable/systems` | Predefined measurement systems (`metric`, `imperial`, `usCustomary`) |
32
37
 
@@ -34,7 +39,7 @@ The package is split into three import paths so the core stays lean:
34
39
 
35
40
  ```ts
36
41
  import { Quantity } from "measurable";
37
- import { meter, mile, celsius, fahrenheit } from "measurable/dimensions";
42
+ import { meter, mile, foot, inch, celsius, fahrenheit } from "measurable/dimensions";
38
43
 
39
44
  // Convert: `.to()` returns a Quantity, `.in()` returns a raw number.
40
45
  new Quantity(5, mile).to(meter).magnitude; // 8046.72
@@ -42,6 +47,10 @@ new Quantity(5, mile).in(meter); // 8046.72
42
47
 
43
48
  // Affine scales work the same way.
44
49
  new Quantity(100, celsius).in(fahrenheit); // 212
50
+
51
+ // Conversions are exact: magnitudes are rationals under the hood.
52
+ new Quantity(1, foot).in(inch); // 12 (not 12.000000000000002)
53
+ new Quantity(1, foot).to(inch).to(foot).magnitude; // 1 (exact round trip)
45
54
  ```
46
55
 
47
56
  ## Concepts
@@ -49,35 +58,110 @@ new Quantity(100, celsius).in(fahrenheit); // 212
49
58
  - **`Dimension`** — a kind of measurable quantity (length, volume, mass, …). It
50
59
  owns a canonical **base unit** and is where all conversion happens. A unit
51
60
  belongs to exactly one dimension.
52
- - **`Unit`** — a name plus a transform (`toBase` / `fromBase`) into its
53
- dimension's base unit. Created through a dimension's builder methods.
54
- - **`Quantity`** a magnitude paired with a unit (e.g. `5 kilometer`).
61
+ - **`Unit`** — a name plus a transform into its dimension's base unit: an exact
62
+ rational `scale`/`offset` for linear and affine units, or an arbitrary
63
+ function pair for `custom` ones. Created through a dimension's builder methods.
64
+ - **`Quantity`** — a magnitude paired with a unit (e.g. `5 kilometer`). The
65
+ magnitude is held as an exact **`Rational`**; `.magnitude` reads it as a
66
+ `number`.
67
+ - **`Rational`** — an exact rational number (`n / d` over `bigint`s) used for the
68
+ lossless arithmetic above. You rarely construct one directly, but can pass one
69
+ anywhere a magnitude or scale is expected.
55
70
  - **`MeasurementSystem`** — a cross-dimension tag (metric/imperial/…). A unit can
56
71
  belong to many; membership is optional and never affects whether a conversion
57
72
  is allowed.
58
73
 
74
+ ## Exact arithmetic
75
+
76
+ A linear conversion is inherently rational — a foot is exactly `3048/10000` m and
77
+ an inch exactly `254/10000` m, so a foot is exactly `12` inches. Storing those
78
+ ratios as binary floats and routing values through the base unit loses that
79
+ (`1 foot → inch` would give `12.000000000000002`).
80
+
81
+ Instead, each `Quantity` keeps its magnitude as an exact `Rational`, and
82
+ conversions/arithmetic run in rational arithmetic, collapsing to a `number` only
83
+ when you read `.magnitude` (or call `.in()`). Because nothing is collapsed
84
+ mid-chain, conversions compose without accumulating drift:
85
+
86
+ ```ts
87
+ import { Quantity } from "measurable";
88
+ import { liter, usGallon } from "measurable/dimensions";
89
+
90
+ // A round trip through an awkward ratio still lands exactly on 7.
91
+ new Quantity(7, liter).to(usGallon).to(liter).magnitude; // 7
92
+
93
+ // .rational exposes the underlying exact value (always in lowest terms).
94
+ new Quantity(7, liter).to(usGallon).rational; // Rational 125000000/67596639
95
+ ```
96
+
97
+ This is exact for linear and affine units. A conversion that passes through a
98
+ non-linear `custom` unit (e.g. a logarithmic scale) necessarily uses floating
99
+ point and recaptures the result as a rational, so it is best-effort there.
100
+
59
101
  ## Built-in dimensions
60
102
 
61
103
  Import any dimension or unit from `measurable/dimensions`:
62
104
 
63
- | Dimension | Base | Units (a selection) |
64
- | ------------- | ---------- | ----------------------------------------------------------------------------------- |
65
- | `length` | `meter` | `kilometer`, `centimeter`, `millimeter`, `inch`, `foot`, `yard`, `mile` |
66
- | `volume` | `liter` | `milliliter`, `us*`/`imperial*` `Gallon`/`Quart`/`Pint`/`Gill`/`FluidOunce`, `cup`, `tablespoon`, `teaspoon` |
67
- | `mass` | `gram` | `kilogram`, `milligram`, `tonne`, `pound`, `ounce`, `stone`, `shortTon`, `longTon` |
68
- | `time` | `second` | `millisecond`, `minute`, `hour`, `day`, `week` |
69
- | `temperature` | `kelvin` | `celsius`, `fahrenheit` |
70
- | `angle` | `radian` | `degree`, `gradian`, `turn` |
71
- | `force` | `newton` | `kilonewton`, `dyne`, `poundForce`, `kilogramForce` |
105
+ | Dimension | Base | Units (a selection) |
106
+ | -------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------ |
107
+ | `length` | `meter` | `kilometer`, `centimeter`, `millimeter`, `inch`, `foot`, `yard`, `mile` |
108
+ | `area` | `squareMeter` | `squareKilometer`, `hectare`, `are`, `squareInch`, `squareFoot`, `squareYard`, `acre`, `squareMile` |
109
+ | `volume` | `liter` | `milliliter`, `us*`/`imperial*` `Gallon`/`Quart`/`Pint`/`Gill`/`FluidOunce`, `cup`, `tablespoon`, `teaspoon` |
110
+ | `mass` | `gram` | `kilogram`, `milligram`, `tonne`, `pound`, `ounce`, `stone`, `shortTon`, `longTon` |
111
+ | `time` | `second` | `millisecond`, `minute`, `hour`, `day`, `week` |
112
+ | `temperature` | `kelvin` | `celsius`, `fahrenheit` |
113
+ | `angle` | `radian` | `degree`, `gradian`, `turn` |
114
+ | `force` | `newton` | `kilonewton`, `dyne`, `poundForce`, `kilogramForce` |
115
+ | `energy` | `joule` | `kilojoule`, `wattHour`, `kilowattHour`, `calorie`, `kilocalorie`, `britishThermalUnit` |
116
+ | `power` | `watt` | `kilowatt`, `megawatt`, `horsepower`, `metricHorsepower` |
117
+ | `pressure` | `pascal` | `kilopascal`, `bar`, `millibar`, `atmosphere`, `torr`, `psi`, `inchOfMercury`, `inchOfWater` |
118
+ | `frequency` | `hertz` | `kilohertz`, `megahertz`, `gigahertz`, `terahertz` |
119
+ | `data` | `bit` | `byte`, `nibble`, `kilobyte`…`petabyte` (SI), `kibibyte`…`pebibyte` (IEC) |
120
+ | `illuminance` | `lux` | `kilolux`, `millilux`, `footCandle`, `phot` |
121
+ | `luminance` | `candelaPerSquareMeter` | `nit`, `stilb` |
122
+ | `luminousIntensity` | `candela` | `kilocandela`, `millicandela`, `candlepower`, `hefnerkerze` |
72
123
 
73
124
  The metric units carry the **full SI prefix ladder** (yotta → yocto), generated for
74
- you: `length`, `mass`, `volume`, and `force` get every prefix (so `kilogram`
75
- itself is just the kilo-prefixed gram), while `time` and `angle` get the
76
- fractional prefixes only (e.g.
77
- `millisecond`, `microradian`). So `decimeter`, `hectometer`, `megagram`,
78
- `kiloliter`, `nanosecond`, etc. are all available and parse from their symbols
79
- (`dm`, `hm`, `Mg`, `kL`, `ns`, and `µm`/`um` for micro). You can apply the same
80
- ladder to your own dimensions with `definePrefixed` (see below).
125
+ you: the SI-based dimensions (`length`, `mass`, `volume`, `force`, `energy`, `power`,
126
+ `pressure`, `frequency`, `illuminance`, `luminousIntensity`) get every prefix (so
127
+ `kilogram` itself is just the kilo-prefixed gram), while `time` and `angle` get the
128
+ fractional prefixes only (e.g. `millisecond`, `microradian`). Every generated rung
129
+ parses from its symbol (`dm`, `hm`, `Mg`, `kL`, `ns`, `GHz`, `kPa`, and `µm`/`um` for
130
+ micro) and converts like any other unit. `data` additionally carries the **IEC binary**
131
+ multiples (`kibibyte`, `mebibyte`, = 1024-based) alongside the SI decimal ones
132
+ (`kilobyte`, … = 1000-based). You can apply the same ladder to your own dimensions with
133
+ `definePrefixed` (see below).
134
+
135
+ ### Prefixed unit exports
136
+
137
+ Each prefixed dimension exports a **record** holding the _complete_ generated ladder
138
+ keyed by name (the return value of `definePrefixed`), plus a **curated subset of those
139
+ same units as individual named exports** for convenience. Reach any rung that isn't
140
+ exported individually through the record — e.g. `metricLength.gigameter`,
141
+ `metricMass.exagram` — or by parsing its symbol.
142
+
143
+ | Dimension | Record export(s) | Individually exported units |
144
+ | ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
145
+ | `length` | `metricLength` | `kilometer`, `hectometer`, `decameter`, `decimeter`, `centimeter`, `millimeter`, `micrometer`, `nanometer` |
146
+ | `mass` | `metricMass` | `kilogram`, `megagram`, `hectogram`, `decagram`, `decigram`, `centigram`, `milligram`, `microgram`, `nanogram` |
147
+ | `volume` | `metricVolume` | `kiloliter`, `hectoliter`, `decaliter`, `deciliter`, `centiliter`, `milliliter` |
148
+ | `time` | `metricTime` | `millisecond`, `microsecond`, `nanosecond`, `picosecond` |
149
+ | `angle` | `metricAngle` | `milliradian`, `microradian` |
150
+ | `force` | `metricForce` | `meganewton`, `kilonewton`, `millinewton`, `micronewton` |
151
+ | `energy` | `metricEnergy`, `metricWattHour` | `kilojoule`, `megajoule`, `gigajoule`, `millijoule`; `kilowattHour`, `megawattHour`, `gigawattHour` |
152
+ | `power` | `metricPower` | `kilowatt`, `megawatt`, `gigawatt`, `terawatt`, `milliwatt` |
153
+ | `pressure` | `metricPressure` | `kilopascal`, `hectopascal`, `megapascal`, `gigapascal` |
154
+ | `frequency` | `metricFrequency` | `kilohertz`, `megahertz`, `gigahertz`, `terahertz`, `petahertz`, `millihertz` |
155
+ | `illuminance` | `metricIlluminance` | `kilolux`, `millilux`, `microlux` |
156
+ | `luminousIntensity` | `metricLuminousIntensity` | `kilocandela`, `millicandela`, `microcandela` |
157
+ | `data` | `dataMultiples` | `kilobit`/`kilobyte` … `petabit`/`petabyte` (SI), `kibibit`/`kibibyte` … `pebibit`/`pebibyte` (IEC) |
158
+
159
+ ```ts
160
+ import { metricLength, kilometer } from "measurable/dimensions";
161
+
162
+ kilometer === metricLength.kilometer; // true — the named export is the same unit
163
+ metricLength.gigameter; // a rung not exported by name, reached via the record
164
+ ```
81
165
 
82
166
  ## Built-in measurement systems
83
167
 
@@ -92,6 +176,13 @@ usCustomary.has(foot); // true
92
176
  metric.has(foot); // false
93
177
  ```
94
178
 
179
+ Membership spans every dimension: SI units (`squareMeter`, `pascal`, `watt`, `joule`,
180
+ `hertz`, `lux`, `candela`, and their prefix ladders) are tagged into `metric`, while the
181
+ customary ones (`squareFoot`, `acre`, `psi`, `horsepower`, `britishThermalUnit`,
182
+ `footCandle`, `candlepower`) belong to both `imperial` and `usCustomary`. Units tied to
183
+ no real-world standard (e.g. `atmosphere`, `torr`, and the `data` multiples) stay
184
+ untagged — they still convert, they just won't appear under any system.
185
+
95
186
  ### Listing units in a system
96
187
 
97
188
  ```ts
@@ -163,9 +254,10 @@ new Quantity(1, shortTon).in(tonne); // 0.90718474
163
254
 
164
255
  Quantities can be combined. `plus`/`minus` take another `Quantity` (converted into
165
256
  the receiver's unit first, so the operands may use different units of the same
166
- dimension); `times`/`dividedBy` apply a dimensionless scalar, and `negate`/`abs`
167
- transform the magnitude. All return a **new** `Quantity` in the receiver's unit and
168
- leave the operands untouched.
257
+ dimension); `times`/`dividedBy` apply a dimensionless scalar (a `number` or a
258
+ `Rational`), and `negate`/`abs` transform the magnitude. All return a **new**
259
+ `Quantity` in the receiver's unit and leave the operands untouched. Like
260
+ conversions, the arithmetic is exact — `q.times(3).dividedBy(3)` returns `q`.
169
261
 
170
262
  ```ts
171
263
  import { Quantity } from "measurable";
@@ -220,8 +312,11 @@ new Quantity(1, kilometer).greaterThan(new Quantity(1, meter)); // true
220
312
 
221
313
  Short aliases: **`eq`** (`equals`), **`ne`** (`notEquals`), **`lt`** (`lessThan`),
222
314
  **`gt`** (`greaterThan`), **`lte`** (`lessThanOrEqual`), **`gte`**
223
- (`greaterThanOrEqual`). Equality is exact, so values differing only by
224
- floating-point rounding from a conversion may compare unequal.
315
+ (`greaterThanOrEqual`). Comparison is exact rational comparison, so quantities
316
+ that are mathematically equal compare equal even when reaching them involved a
317
+ conversion that would have drifted in floating point — e.g.
318
+ `new Quantity(7, liter).to(usGallon).to(liter).equals(new Quantity(7, liter))` is
319
+ `true`.
225
320
 
226
321
  `compareTo(other)` returns `-1`, `0`, or `1`, suitable as an `Array#sort`
227
322
  comparator: `quantities.sort((a, b) => a.compareTo(b))`.
@@ -261,13 +356,35 @@ const megabyte = data.unit("megabyte", 1024 ** 2, ["MB"]);
261
356
  new Quantity(2, megabyte).in(kilobyte); // 2048
262
357
  ```
263
358
 
359
+ A numeric `scale` is read as the exact decimal you wrote (`0.0254` → `254/10000`),
360
+ which is exact for any terminating decimal. For a ratio a decimal **can't**
361
+ represent exactly — e.g. `5/9` — pass a `Rational` so it stays exact:
362
+
363
+ ```ts
364
+ import { Dimension, Rational } from "measurable";
365
+
366
+ const ratio = new Dimension("ratio");
367
+ ratio.base("whole");
368
+ const third = ratio.unit("third", new Rational(1, 3)); // exact, not 0.3333…
369
+ ```
370
+
264
371
  ### Affine units (offset, not just scale)
265
372
 
266
373
  ```ts
374
+ import { Dimension, Rational } from "measurable";
375
+
267
376
  const temperature = new Dimension("temperature");
268
377
  const kelvin = temperature.base("kelvin", ["K"]);
269
378
  // value_in_base = value * scale + offset
270
379
  const celsius = temperature.affine("celsius", { scale: 1, offset: 273.15 }, ["C"]);
380
+ // Fahrenheit's 5/9 isn't a terminating decimal — give it (and the derived
381
+ // offset) as exact Rationals so conversions round-trip without drift.
382
+ const scale = new Rational(5, 9);
383
+ const fahrenheit = temperature.affine(
384
+ "fahrenheit",
385
+ { scale, offset: Rational.from(273.15).minus(new Rational(32).times(scale)) },
386
+ ["F"],
387
+ );
271
388
  ```
272
389
 
273
390
  ### Fully custom transforms
@@ -314,10 +431,11 @@ si.has(kilobyte); // true
314
431
 
315
432
  - `new Dimension(name)`
316
433
  - `.base(name, aliases?)` — define the canonical base unit
317
- - `.unit(name, scale, aliases?)` — linear unit (`scale` base units per unit)
318
- - `.affine(name, { scale, offset }, aliases?)` — linear with additive offset
319
- - `.custom(name, { toBase, fromBase }, aliases?)` — arbitrary inverse pair
320
- - `.convert(value, from, to)` — convert a raw number between two of its units
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
437
+ - `.convert(value, from, to)` — convert a raw `number` between two of its units
438
+ - `.convertRational(value, from, to)` → `Rational` — exact conversion between two of its units
321
439
  - `.get(token)` — units matching a name/alias (`Unit[] | undefined`)
322
440
  - `.has(unit)`, `.units`, `.baseUnit`
323
441
 
@@ -328,17 +446,20 @@ A passive handle, normally created via a dimension's builder methods rather than
328
446
 
329
447
  - `.name` — the unit's canonical name
330
448
  - `.dimension` — the `Dimension` it belongs to
449
+ - `.linear` → `{ scale: Rational; offset: Rational } | undefined` — the exact transform for linear/affine units (`undefined` for `custom` ones)
331
450
  - `.toBase(value)` → `number` — convert a value in this unit to base units
332
451
  - `.fromBase(value)` → `number` — convert a value in base units to this unit
333
452
 
334
453
  ### `Quantity`
335
454
 
336
- - `new Quantity(magnitude, unit)`
455
+ - `new Quantity(magnitude, unit)` — `magnitude` is a `number | Rational`; throws on a non-finite `number`
456
+ - `.magnitude` → `number` — getter, derived from `.rational`
457
+ - `.rational` → `Rational` — the exact magnitude (source of truth)
337
458
  - `.to(target)` → `Quantity`
338
459
  - `.in(target)` → `number`
339
460
  - `.toString()` → `string` — e.g. `"5 kilometer"`
340
461
  - `.plus(other)` / `.minus(other)` → `Quantity` — add/subtract another quantity (aliases: `add` / `sub`)
341
- - `.times(factor)` / `.dividedBy(divisor)` → `Quantity` — scale by a number (aliases: `mul` / `div`)
462
+ - `.times(factor)` / `.dividedBy(divisor)` → `Quantity` — scale by a `number | Rational` (aliases: `mul` / `div`)
342
463
  - `.ratioTo(other)` → `number` — dimensionless ratio (how many of `other` fit in this)
343
464
  - `.negate()` / `.abs()` → `Quantity`
344
465
  - `.clamp(lower, upper)` → `Quantity` — bound to a range, in this unit
@@ -351,6 +472,23 @@ A passive handle, normally created via a dimension's builder methods rather than
351
472
  - `Quantity.min(...quantities)` / `Quantity.max(...quantities)` / `Quantity.sum(...quantities)` → `Quantity`
352
473
  - `Quantity.parse(input, dimension, { prefer? })` → `Quantity`
353
474
 
475
+ ### `Rational`
476
+
477
+ An exact rational number (`n / d`), stored as `bigint`s in lowest terms with a
478
+ positive denominator. Immutable; every operation returns a new `Rational`. Used
479
+ internally for lossless conversions and arithmetic, but you can construct one to
480
+ pass anywhere a magnitude or scale is accepted.
481
+
482
+ - `new Rational(numerator, denominator?)` — from integers (`bigint | number`; denominator defaults to `1`). Throws on a non-integer `number` or a zero denominator.
483
+ - `Rational.from(value)` — coerce a `number | Rational` (a `number` is read as its exact terminating decimal, e.g. `0.0254` → `254/10000`)
484
+ - `.n` / `.d` → `bigint` — numerator and denominator
485
+ - `.plus(other)` / `.minus(other)` / `.times(other)` / `.dividedBy(other)` → `Rational` (aliases: `add` / `sub` / `mul` / `div`)
486
+ - `.negate()` / `.abs()` → `Rational`
487
+ - `.equals(other)` → `boolean` (alias: `eq`)
488
+ - `.compare(other)` → `-1 | 0 | 1`
489
+ - `.sign()` → `-1 | 0 | 1`
490
+ - `.toNumber()` → `number` — collapse to the nearest `number`
491
+
354
492
  ### `MeasurementSystem`
355
493
 
356
494
  - `new MeasurementSystem(name)`
@@ -364,6 +502,12 @@ A passive handle, normally created via a dimension's builder methods rather than
364
502
  - `UnknownUnitError` — a parsed token matches no unit
365
503
  - `AmbiguousUnitError` — a parsed token matches several units and no `prefer` was given
366
504
 
505
+ ## Changelog
506
+
507
+ See [CHANGELOG.md](https://github.com/mhuggins/measurable/blob/main/CHANGELOG.md)
508
+ for release notes. Note that v2.0.0 is a breaking release — see its entry for the
509
+ migration details.
510
+
367
511
  ## License
368
512
 
369
513
  ISC
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.microradian = exports.milliradian = exports.metricAngle = exports.turn = exports.gradian = exports.degree = exports.radian = exports.angle = void 0;
4
4
  const Dimension_1 = require("../lib/Dimension");
5
- const prefixes_1 = require("../lib/prefixes");
5
+ const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Plane angle. Base unit: radian. */
7
7
  exports.angle = new Dimension_1.Dimension("angle");
8
8
  exports.radian = exports.angle.base("radian", ["rad", "radians"]);
@@ -10,5 +10,5 @@ exports.degree = exports.angle.unit("degree", Math.PI / 180, ["deg", "°", "degr
10
10
  exports.gradian = exports.angle.unit("gradian", Math.PI / 200, ["grad", "gradians"]);
11
11
  exports.turn = exports.angle.unit("turn", 2 * Math.PI, ["turns", "revolution", "revolutions"]);
12
12
  /** SI-submultiple radians (milliradian, microradian, …); larger angles use degree/turn. */
13
- exports.metricAngle = (0, prefixes_1.definePrefixed)(exports.angle, { name: "radian", symbol: "rad", scale: 1 }, prefixes_1.SI_SUBMULTIPLE_PREFIXES);
13
+ exports.metricAngle = (0, definePrefixed_1.definePrefixed)(exports.angle, { name: "radian", symbol: "rad" }, definePrefixed_1.SI_SUBMULTIPLE_PREFIXES);
14
14
  exports.milliradian = exports.metricAngle.milliradian, exports.microradian = exports.metricAngle.microradian;
@@ -0,0 +1,14 @@
1
+ import { Dimension } from "../lib/Dimension";
2
+ /** Area. Base unit: square meter. */
3
+ export declare const area: Dimension;
4
+ export declare const squareMeter: import("..").Unit;
5
+ export declare const squareKilometer: import("..").Unit;
6
+ export declare const hectare: import("..").Unit;
7
+ export declare const are: import("..").Unit;
8
+ export declare const squareCentimeter: import("..").Unit;
9
+ export declare const squareMillimeter: import("..").Unit;
10
+ export declare const squareInch: import("..").Unit;
11
+ export declare const squareFoot: import("..").Unit;
12
+ export declare const squareYard: import("..").Unit;
13
+ export declare const acre: import("..").Unit;
14
+ export declare const squareMile: import("..").Unit;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.squareMile = exports.acre = exports.squareYard = exports.squareFoot = exports.squareInch = exports.squareMillimeter = exports.squareCentimeter = exports.are = exports.hectare = exports.squareKilometer = exports.squareMeter = exports.area = void 0;
4
+ const Dimension_1 = require("../lib/Dimension");
5
+ /** Area. Base unit: square meter. */
6
+ exports.area = new Dimension_1.Dimension("area");
7
+ exports.squareMeter = exports.area.base("squareMeter", ["m²", "m2", "square meter", "square meters"]);
8
+ // Metric multiples/submultiples. Area scales as the square of the length
9
+ // 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"]);
15
+ // 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"]);
@@ -0,0 +1,10 @@
1
+ import { Dimension } from "../lib/Dimension";
2
+ import type { Unit } from "../lib/Unit";
3
+ /** Digital information. Base unit: bit. */
4
+ export declare const data: Dimension;
5
+ export declare const bit: Unit;
6
+ export declare const nibble: Unit;
7
+ export declare const byte: Unit;
8
+ /** Every SI and IEC multiple of the bit and byte, keyed by name. */
9
+ export declare const dataMultiples: Record<string, Unit>;
10
+ export declare const kilobit: Unit, kilobyte: Unit, megabit: Unit, megabyte: Unit, gigabit: Unit, gigabyte: Unit, terabit: Unit, terabyte: Unit, petabit: Unit, petabyte: Unit, kibibit: Unit, kibibyte: Unit, mebibit: Unit, mebibyte: Unit, gibibit: Unit, gibibyte: Unit, tebibit: Unit, tebibyte: Unit, pebibit: Unit, pebibyte: Unit;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pebibyte = exports.pebibit = exports.tebibyte = exports.tebibit = exports.gibibyte = exports.gibibit = exports.mebibyte = exports.mebibit = exports.kibibyte = exports.kibibit = exports.petabyte = exports.petabit = exports.terabyte = exports.terabit = exports.gigabyte = exports.gigabit = exports.megabyte = exports.megabit = exports.kilobyte = exports.kilobit = exports.dataMultiples = exports.byte = exports.nibble = exports.bit = exports.data = void 0;
4
+ const Dimension_1 = require("../lib/Dimension");
5
+ /** Digital information. Base unit: bit. */
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"]);
10
+ // SI (decimal, 1000-based) and IEC (binary, 1024-based) multiples of the bit
11
+ // and byte. SI uses the bare symbol (kb, kB); IEC uses the "i" infix (Kib, KiB).
12
+ const SI_MULTIPLES = [
13
+ ["kilo", "k", 1e3],
14
+ ["mega", "M", 1e6],
15
+ ["giga", "G", 1e9],
16
+ ["tera", "T", 1e12],
17
+ ["peta", "P", 1e15],
18
+ ];
19
+ const IEC_MULTIPLES = [
20
+ ["kibi", "Ki", 2 ** 10],
21
+ ["mebi", "Mi", 2 ** 20],
22
+ ["gibi", "Gi", 2 ** 30],
23
+ ["tebi", "Ti", 2 ** 40],
24
+ ["pebi", "Pi", 2 ** 50],
25
+ ];
26
+ const multiples = {};
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
+ ]);
33
+ }
34
+ /** Every SI and IEC multiple of the bit and byte, keyed by name. */
35
+ exports.dataMultiples = multiples;
36
+ exports.kilobit = multiples.kilobit, exports.kilobyte = multiples.kilobyte, exports.megabit = multiples.megabit, exports.megabyte = multiples.megabyte, exports.gigabit = multiples.gigabit, exports.gigabyte = multiples.gigabyte, exports.terabit = multiples.terabit, exports.terabyte = multiples.terabyte, exports.petabit = multiples.petabit, exports.petabyte = multiples.petabyte, exports.kibibit = multiples.kibibit, exports.kibibyte = multiples.kibibyte, exports.mebibit = multiples.mebibit, exports.mebibyte = multiples.mebibyte, exports.gibibit = multiples.gibibit, exports.gibibyte = multiples.gibibyte, exports.tebibit = multiples.tebibit, exports.tebibyte = multiples.tebibyte, exports.pebibit = multiples.pebibit, exports.pebibyte = multiples.pebibyte;
@@ -0,0 +1,14 @@
1
+ import { Dimension } from "../lib/Dimension";
2
+ /** Energy. Base unit: joule. */
3
+ export declare const energy: Dimension;
4
+ export declare const joule: import("..").Unit;
5
+ export declare const calorie: import("..").Unit;
6
+ export declare const kilocalorie: import("..").Unit;
7
+ export declare const britishThermalUnit: import("..").Unit;
8
+ export declare const wattHour: import("..").Unit;
9
+ /** Every SI-prefixed joule (kilojoule, megajoule, millijoule, …), keyed by name. */
10
+ export declare const metricEnergy: Record<string, import("..").Unit>;
11
+ export declare const kilojoule: import("..").Unit, megajoule: import("..").Unit, gigajoule: import("..").Unit, millijoule: import("..").Unit;
12
+ /** Every SI-prefixed watt-hour (kilowatt-hour, megawatt-hour, …), keyed by name. */
13
+ export declare const metricWattHour: Record<string, import("..").Unit>;
14
+ export declare const kilowattHour: import("..").Unit, megawattHour: import("..").Unit, gigawattHour: import("..").Unit;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gigawattHour = exports.megawattHour = exports.kilowattHour = exports.metricWattHour = exports.millijoule = exports.gigajoule = exports.megajoule = exports.kilojoule = exports.metricEnergy = exports.wattHour = exports.britishThermalUnit = exports.kilocalorie = exports.calorie = exports.joule = exports.energy = void 0;
4
+ const Dimension_1 = require("../lib/Dimension");
5
+ const definePrefixed_1 = require("../utils/definePrefixed");
6
+ /** Energy. Base unit: joule. */
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"]);
13
+ /** Every SI-prefixed joule (kilojoule, megajoule, millijoule, …), keyed by name. */
14
+ exports.metricEnergy = (0, definePrefixed_1.definePrefixed)(exports.energy, { name: "joule", symbol: "J" });
15
+ exports.kilojoule = exports.metricEnergy.kilojoule, exports.megajoule = exports.metricEnergy.megajoule, exports.gigajoule = exports.metricEnergy.gigajoule, exports.millijoule = exports.metricEnergy.millijoule;
16
+ /** 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
+ });
22
+ exports.kilowattHour = exports.metricWattHour.kilowattHour, exports.megawattHour = exports.metricWattHour.megawattHour, exports.gigawattHour = exports.metricWattHour.gigawattHour;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.micronewton = exports.millinewton = exports.kilonewton = exports.meganewton = exports.metricForce = exports.kilogramForce = exports.poundForce = exports.dyne = exports.newton = exports.force = void 0;
4
4
  const Dimension_1 = require("../lib/Dimension");
5
- const prefixes_1 = require("../lib/prefixes");
5
+ const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Force. Base unit: newton. */
7
7
  exports.force = new Dimension_1.Dimension("force");
8
8
  exports.newton = exports.force.base("newton", ["N", "newtons"]);
@@ -10,5 +10,5 @@ exports.dyne = exports.force.unit("dyne", 0.00001, ["dyn", "dynes"]);
10
10
  exports.poundForce = exports.force.unit("poundForce", 4.4482216152605, ["lbf"]);
11
11
  exports.kilogramForce = exports.force.unit("kilogramForce", 9.80665, ["kgf"]);
12
12
  /** Every SI-prefixed newton (kilonewton, meganewton, millinewton, …), keyed by name. */
13
- exports.metricForce = (0, prefixes_1.definePrefixed)(exports.force, { name: "newton", symbol: "N", scale: 1 });
13
+ exports.metricForce = (0, definePrefixed_1.definePrefixed)(exports.force, { name: "newton", symbol: "N" });
14
14
  exports.meganewton = exports.metricForce.meganewton, exports.kilonewton = exports.metricForce.kilonewton, exports.millinewton = exports.metricForce.millinewton, exports.micronewton = exports.metricForce.micronewton;
@@ -0,0 +1,7 @@
1
+ import { Dimension } from "../lib/Dimension";
2
+ /** Frequency. Base unit: hertz. */
3
+ export declare const frequency: Dimension;
4
+ export declare const hertz: import("..").Unit;
5
+ /** Every SI-prefixed hertz (kilohertz, megahertz, gigahertz, …), keyed by name. */
6
+ export declare const metricFrequency: Record<string, import("..").Unit>;
7
+ export declare const kilohertz: import("..").Unit, megahertz: import("..").Unit, gigahertz: import("..").Unit, terahertz: import("..").Unit, petahertz: import("..").Unit, millihertz: import("..").Unit;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.millihertz = exports.petahertz = exports.terahertz = exports.gigahertz = exports.megahertz = exports.kilohertz = exports.metricFrequency = exports.hertz = exports.frequency = void 0;
4
+ const Dimension_1 = require("../lib/Dimension");
5
+ const definePrefixed_1 = require("../utils/definePrefixed");
6
+ /** Frequency. Base unit: hertz. */
7
+ exports.frequency = new Dimension_1.Dimension("frequency");
8
+ exports.hertz = exports.frequency.base("hertz", ["Hz"]);
9
+ /** Every SI-prefixed hertz (kilohertz, megahertz, gigahertz, …), keyed by name. */
10
+ exports.metricFrequency = (0, definePrefixed_1.definePrefixed)(exports.frequency, { name: "hertz", symbol: "Hz" });
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;
@@ -0,0 +1,9 @@
1
+ import { Dimension } from "../lib/Dimension";
2
+ /** Illuminance. Base unit: lux. */
3
+ export declare const illuminance: Dimension;
4
+ export declare const lux: import("..").Unit;
5
+ export declare const footCandle: import("..").Unit;
6
+ export declare const phot: import("..").Unit;
7
+ /** Every SI-prefixed lux (kilolux, millilux, microlux, …), keyed by name. */
8
+ export declare const metricIlluminance: Record<string, import("..").Unit>;
9
+ export declare const kilolux: import("..").Unit, millilux: import("..").Unit, microlux: import("..").Unit;