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
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.usCustomary = void 0;
4
+ const area_1 = require("../dimensions/area");
5
+ const energy_1 = require("../dimensions/energy");
6
+ const illuminance_1 = require("../dimensions/illuminance");
4
7
  const length_1 = require("../dimensions/length");
8
+ const luminousIntensity_1 = require("../dimensions/luminousIntensity");
5
9
  const mass_1 = require("../dimensions/mass");
10
+ const power_1 = require("../dimensions/power");
11
+ const pressure_1 = require("../dimensions/pressure");
6
12
  const temperature_1 = require("../dimensions/temperature");
7
13
  const volume_1 = require("../dimensions/volume");
8
14
  const MeasurementSystem_1 = require("../lib/MeasurementSystem");
@@ -10,9 +16,21 @@ const MeasurementSystem_1 = require("../lib/MeasurementSystem");
10
16
  exports.usCustomary = new MeasurementSystem_1.MeasurementSystem("usCustomary").add(
11
17
  // length
12
18
  length_1.inch, length_1.foot, length_1.yard, length_1.mile,
19
+ // area
20
+ area_1.squareInch, area_1.squareFoot, area_1.squareYard, area_1.acre, area_1.squareMile,
13
21
  // mass
14
22
  mass_1.pound, mass_1.ounce, mass_1.shortTon,
15
23
  // volume
16
24
  volume_1.usGallon, volume_1.usQuart, volume_1.usPint, volume_1.usGill, volume_1.usFluidOunce, volume_1.cup, volume_1.tablespoon, volume_1.teaspoon,
17
25
  // temperature
18
- temperature_1.fahrenheit);
26
+ temperature_1.fahrenheit,
27
+ // energy
28
+ energy_1.britishThermalUnit,
29
+ // power
30
+ power_1.horsepower,
31
+ // pressure
32
+ pressure_1.psi, pressure_1.inchOfMercury, pressure_1.inchOfWater,
33
+ // illuminance
34
+ illuminance_1.footCandle,
35
+ // luminous intensity
36
+ luminousIntensity_1.candlepower);
@@ -0,0 +1,35 @@
1
+ import type { Dimension } from "../lib/Dimension";
2
+ import { Rational } from "../lib/Rational";
3
+ import type { Unit } from "../lib/Unit";
4
+ /** A metric (SI) prefix: a name, symbol, and power-of-ten factor. */
5
+ export interface SiPrefix {
6
+ name: string;
7
+ symbol: string;
8
+ factor: number;
9
+ }
10
+ /** The full set of SI prefixes, yotta (1e24) down to yocto (1e-24). */
11
+ export declare const SI_PREFIXES: readonly SiPrefix[];
12
+ /** SI prefixes for fractions only (deci and smaller) — for units like seconds. */
13
+ export declare const SI_SUBMULTIPLE_PREFIXES: readonly SiPrefix[];
14
+ /** The unit a set of metric prefixes is generated relative to. */
15
+ export interface PrefixReference {
16
+ /** Singular unit name, e.g. "meter" → "kilometer", "millimeter", … */
17
+ name: string;
18
+ /** Primary symbol, e.g. "m" → "km", "mm", … */
19
+ symbol: string;
20
+ /** Scale of the reference relative to its dimension's base (meter → 1, gram → 0.001) (default 1). */
21
+ scale?: number | Rational;
22
+ }
23
+ /**
24
+ * Define metric-prefixed variants of a reference unit on a dimension. Each
25
+ * variant is named `${prefix}${reference.name}` (e.g. "kilometer") with scale
26
+ * `reference.scale * prefix.factor`, plus a `${prefix.symbol}${reference.symbol}`
27
+ * alias and a plural (and an ASCII "u" form for micro).
28
+ *
29
+ * Prefixes whose generated name already exists on the dimension are skipped, so
30
+ * a base like "kilogram" is left intact when prefixing "gram".
31
+ *
32
+ * Returns the created units keyed by name, for spreading into a
33
+ * {@link MeasurementSystem} or destructuring into named exports.
34
+ */
35
+ export declare function definePrefixed(dimension: Dimension, reference: PrefixReference, prefixes?: readonly SiPrefix[]): Record<string, Unit>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SI_SUBMULTIPLE_PREFIXES = exports.SI_PREFIXES = void 0;
4
+ exports.definePrefixed = definePrefixed;
5
+ const Rational_1 = require("../lib/Rational");
6
+ /** The full set of SI prefixes, yotta (1e24) down to yocto (1e-24). */
7
+ exports.SI_PREFIXES = [
8
+ { name: "yotta", symbol: "Y", factor: 1e24 },
9
+ { name: "zetta", symbol: "Z", factor: 1e21 },
10
+ { name: "exa", symbol: "E", factor: 1e18 },
11
+ { name: "peta", symbol: "P", factor: 1e15 },
12
+ { name: "tera", symbol: "T", factor: 1e12 },
13
+ { name: "giga", symbol: "G", factor: 1e9 },
14
+ { name: "mega", symbol: "M", factor: 1e6 },
15
+ { name: "kilo", symbol: "k", factor: 1e3 },
16
+ { name: "hecto", symbol: "h", factor: 1e2 },
17
+ { name: "deca", symbol: "da", factor: 1e1 },
18
+ { name: "deci", symbol: "d", factor: 1e-1 },
19
+ { name: "centi", symbol: "c", factor: 1e-2 },
20
+ { name: "milli", symbol: "m", factor: 1e-3 },
21
+ { name: "micro", symbol: "µ", factor: 1e-6 },
22
+ { name: "nano", symbol: "n", factor: 1e-9 },
23
+ { name: "pico", symbol: "p", factor: 1e-12 },
24
+ { name: "femto", symbol: "f", factor: 1e-15 },
25
+ { name: "atto", symbol: "a", factor: 1e-18 },
26
+ { name: "zepto", symbol: "z", factor: 1e-21 },
27
+ { name: "yocto", symbol: "y", factor: 1e-24 },
28
+ ];
29
+ /** SI prefixes for fractions only (deci and smaller) — for units like seconds. */
30
+ exports.SI_SUBMULTIPLE_PREFIXES = exports.SI_PREFIXES.filter((prefix) => prefix.factor < 1);
31
+ /**
32
+ * Define metric-prefixed variants of a reference unit on a dimension. Each
33
+ * variant is named `${prefix}${reference.name}` (e.g. "kilometer") with scale
34
+ * `reference.scale * prefix.factor`, plus a `${prefix.symbol}${reference.symbol}`
35
+ * alias and a plural (and an ASCII "u" form for micro).
36
+ *
37
+ * Prefixes whose generated name already exists on the dimension are skipped, so
38
+ * a base like "kilogram" is left intact when prefixing "gram".
39
+ *
40
+ * Returns the created units keyed by name, for spreading into a
41
+ * {@link MeasurementSystem} or destructuring into named exports.
42
+ */
43
+ function definePrefixed(dimension, reference, prefixes = exports.SI_PREFIXES) {
44
+ const units = {};
45
+ for (const prefix of prefixes) {
46
+ const name = `${prefix.name}${reference.name}`;
47
+ if (dimension.get(name)) {
48
+ continue;
49
+ }
50
+ const aliases = [`${prefix.symbol}${reference.symbol}`, `${name}s`];
51
+ if (prefix.name === "micro") {
52
+ aliases.push(`u${reference.symbol}`);
53
+ }
54
+ // Multiply as rationals: each factor (a power of ten, or an exact reference
55
+ // scale) is lossless on its own, but multiplying them as floats can drift
56
+ // (e.g. 3600 * 1e-9). Rational multiplication keeps the prefixed scale exact.
57
+ const scale = Rational_1.Rational.from(reference.scale ?? 1).times(Rational_1.Rational.from(prefix.factor));
58
+ units[name] = dimension.unit(name, scale, aliases);
59
+ }
60
+ return units;
61
+ }
@@ -0,0 +1,3 @@
1
+ import type { Unit } from "../lib/Unit";
2
+ /** Pure linear scale of a unit relative to base, ignoring any affine offset. */
3
+ export declare const scaleOf: (unit: Unit) => number;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scaleOf = void 0;
4
+ /** Pure linear scale of a unit relative to base, ignoring any affine offset. */
5
+ const scaleOf = (unit) => unit.linear ? unit.linear.scale.toNumber() : unit.toBase(1) - unit.toBase(0);
6
+ exports.scaleOf = scaleOf;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "measurable",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Convert between units of measurement with custom and built-in systems",
5
5
  "keywords": [
6
6
  "conversion",
@@ -17,6 +17,9 @@
17
17
  "author": "Matt Huggins <matt.huggins@gmail.com>",
18
18
  "license": "ISC",
19
19
  "repository": "https://github.com/mhuggins/measurable",
20
+ "engines": {
21
+ "node": ">=14"
22
+ },
20
23
  "main": "dist/index.js",
21
24
  "types": "dist/index.d.ts",
22
25
  "exports": {
@@ -34,7 +37,8 @@
34
37
  }
35
38
  },
36
39
  "files": [
37
- "dist"
40
+ "dist",
41
+ "CHANGELOG.md"
38
42
  ],
39
43
  "devDependencies": {
40
44
  "@biomejs/biome": "^2.5.0",
@@ -49,5 +53,6 @@
49
53
  "format": "biome format .",
50
54
  "typecheck": "tsc --noEmit",
51
55
  "test": "vitest run"
52
- }
56
+ },
57
+ "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\nA `Quantity` also has a `toString()` that renders `\"<magnitude> <unit name>\"`\n(e.g. `new Quantity(5, kilometer).toString()` → `\"5 kilometer\"`), and `round(decimals)`\nto trim the magnitude for display (`new Quantity(1.6213, mile).round(2)` → `1.62 mile`).\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.\n\n```ts\nimport { Dimension, Quantity } from \"measurable\";\n\nconst data = new Dimension(\"data\");\nconst byte = data.base(\"byte\", [\"B\", \"bytes\"]); // the base unit (identity)\nconst kilobyte = data.unit(\"kilobyte\", 1024, [\"KB\"]);\nconst megabyte = data.unit(\"megabyte\", 1024 ** 2, [\"MB\"]);\n\nnew Quantity(2, megabyte).in(kilobyte); // 2048\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\", [\"K\"]);\n// value_in_base = value * scale + offset\nconst celsius = temperature.affine(\"celsius\", { scale: 1, offset: 273.15 }, [\"C\"]);\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 [\"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). Pass\n`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\", [\"b\"]);\nconst prefixed = definePrefixed(data, { name: \"bit\", symbol: \"b\", scale: 1 });\n\nnew Quantity(1, prefixed.kilobit).in(bit); // 1000 (SI kilo = 1e3)\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, aliases?)` — define the canonical base unit\n- `.unit(name, scale, aliases?)` — linear unit (`scale` base units per unit; `number | Rational`)\n- `.affine(name, { scale, offset }, aliases?)` — linear with additive offset (each `number | Rational`)\n- `.custom(name, { toBase, fromBase }, aliases?)` — 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### `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- `.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` — e.g. `\"5 kilometer\"`\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"
53
58
  }