measurable 2.0.0 → 3.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.
- package/CHANGELOG.md +49 -0
- package/README.md +96 -22
- package/dist/dimensions/angle.js +12 -5
- package/dist/dimensions/area.js +37 -16
- package/dist/dimensions/data.js +11 -8
- package/dist/dimensions/energy.js +18 -11
- package/dist/dimensions/force.js +5 -5
- package/dist/dimensions/frequency.js +2 -2
- package/dist/dimensions/illuminance.js +7 -4
- package/dist/dimensions/length.js +6 -6
- package/dist/dimensions/luminance.js +5 -8
- package/dist/dimensions/luminousIntensity.js +7 -7
- package/dist/dimensions/mass.js +12 -8
- package/dist/dimensions/power.js +4 -4
- package/dist/dimensions/pressure.js +12 -9
- package/dist/dimensions/temperature.js +3 -3
- package/dist/dimensions/time.js +18 -6
- package/dist/dimensions/volume.js +56 -23
- package/dist/lib/Dimension.d.ts +14 -5
- package/dist/lib/Dimension.js +18 -16
- package/dist/lib/Quantity.d.ts +70 -0
- package/dist/lib/Quantity.js +53 -0
- package/dist/lib/Rational.d.ts +1 -1
- package/dist/lib/Rational.js +1 -1
- package/dist/lib/Unit.d.ts +18 -11
- package/dist/lib/Unit.js +7 -3
- package/dist/utils/definePrefixed.d.ts +8 -15
- package/dist/utils/definePrefixed.js +21 -10
- package/package.json +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "measurable",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Convert between units of measurement with custom and built-in systems",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conversion",
|
|
@@ -42,17 +42,22 @@
|
|
|
42
42
|
],
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@biomejs/biome": "^2.5.0",
|
|
45
|
+
"typedoc": "^0.28.19",
|
|
45
46
|
"typescript": "^6.0.3",
|
|
46
47
|
"vite": "^8.0.16",
|
|
47
48
|
"vitest": "^4.1.9"
|
|
48
49
|
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"assert-never": "^1.4.0"
|
|
52
|
+
},
|
|
49
53
|
"scripts": {
|
|
50
|
-
"prebuild": "pnpm run lint && pnpm run format && pnpm run typecheck && pnpm run test",
|
|
54
|
+
"prebuild": "pnpm run lint && pnpm run format && pnpm run typecheck && pnpm run lint:docs && pnpm run test",
|
|
51
55
|
"build": "tsc",
|
|
52
56
|
"lint": "biome check .",
|
|
57
|
+
"lint:docs": "typedoc",
|
|
53
58
|
"format": "biome format .",
|
|
54
59
|
"typecheck": "tsc --noEmit",
|
|
55
60
|
"test": "vitest run"
|
|
56
61
|
},
|
|
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"
|
|
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"
|
|
58
63
|
}
|