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.
@@ -5,14 +5,26 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Time / duration. Base unit: second. */
7
7
  exports.time = new Dimension_1.Dimension("time");
8
- exports.second = exports.time.base("second", ["s", "sec", "secs", "seconds"]);
9
- exports.minute = exports.time.unit("minute", 60, ["min", "mins", "minutes"]);
10
- exports.hour = exports.time.unit("hour", 3600, ["h", "hr", "hrs", "hours"]);
11
- exports.day = exports.time.unit("day", 86400, ["d", "days"]);
12
- exports.week = exports.time.unit("week", 604800, ["wk", "weeks"]);
8
+ exports.second = exports.time.base("second", {
9
+ symbol: "s",
10
+ plural: "seconds",
11
+ aliases: ["sec", "secs"],
12
+ });
13
+ exports.minute = exports.time.unit("minute", 60, {
14
+ symbol: "min",
15
+ plural: "minutes",
16
+ aliases: ["mins"],
17
+ });
18
+ exports.hour = exports.time.unit("hour", 3600, {
19
+ symbol: "h",
20
+ plural: "hours",
21
+ aliases: ["hr", "hrs"],
22
+ });
23
+ exports.day = exports.time.unit("day", 86400, { symbol: "d", plural: "days" });
24
+ exports.week = exports.time.unit("week", 604800, { symbol: "wk", plural: "weeks" });
13
25
  /**
14
26
  * SI-submultiple seconds (millisecond, microsecond, nanosecond, …). Only
15
27
  * fractions are generated; larger spans use minute/hour/day/week above.
16
28
  */
17
- exports.metricTime = (0, definePrefixed_1.definePrefixed)(exports.time, { name: "second", symbol: "s" }, definePrefixed_1.SI_SUBMULTIPLE_PREFIXES);
29
+ exports.metricTime = (0, definePrefixed_1.definePrefixed)(exports.time, exports.second, definePrefixed_1.SI_SUBMULTIPLE_PREFIXES);
18
30
  exports.millisecond = exports.metricTime.millisecond, exports.microsecond = exports.metricTime.microsecond, exports.nanosecond = exports.metricTime.nanosecond, exports.picosecond = exports.metricTime.picosecond;
@@ -5,33 +5,66 @@ const Dimension_1 = require("../lib/Dimension");
5
5
  const definePrefixed_1 = require("../utils/definePrefixed");
6
6
  /** Volume / capacity. Base unit: liter. */
7
7
  exports.volume = new Dimension_1.Dimension("volume");
8
- exports.liter = exports.volume.base("liter", ["L", "liters"]);
8
+ exports.liter = exports.volume.base("liter", { symbol: "L", plural: "liters" });
9
9
  // US and Imperial liquid measures share names ("gallon", "pint", …) but differ
10
10
  // in size, so each is a distinct unit carrying the shared aliases; parsing
11
11
  // disambiguates via a preferred measurement system. Imperial has 160 fluid
12
12
  // ounces per gallon, US customary has 128.
13
- exports.usGallon = exports.volume.unit("usGallon", 3.785411784, ["gal", "gallon", "gallons"]);
14
- exports.usQuart = exports.volume.unit("usQuart", 0.946352946, ["qt", "quart", "quarts"]);
15
- exports.usPint = exports.volume.unit("usPint", 0.473176473, ["pt", "pint", "pints"]);
16
- exports.usGill = exports.volume.unit("usGill", 0.11829411825, ["gill", "gills"]);
17
- exports.usFluidOunce = exports.volume.unit("usFluidOunce", 0.0295735295625, [
18
- "floz",
19
- "fluidOunce",
20
- "fluidOunces",
21
- ]);
22
- exports.imperialGallon = exports.volume.unit("imperialGallon", 4.54609, ["gal", "gallon", "gallons"]);
23
- exports.imperialQuart = exports.volume.unit("imperialQuart", 1.1365225, ["qt", "quart", "quarts"]);
24
- exports.imperialPint = exports.volume.unit("imperialPint", 0.56826125, ["pt", "pint", "pints"]);
25
- exports.imperialGill = exports.volume.unit("imperialGill", 0.1420653125, ["gill", "gills"]);
26
- exports.imperialFluidOunce = exports.volume.unit("imperialFluidOunce", 0.0284130625, [
27
- "floz",
28
- "fluidOunce",
29
- "fluidOunces",
30
- ]);
13
+ exports.usGallon = exports.volume.unit("usGallon", 3.785411784, {
14
+ symbol: "gal",
15
+ plural: "gallons",
16
+ aliases: ["gallon"],
17
+ });
18
+ exports.usQuart = exports.volume.unit("usQuart", 0.946352946, {
19
+ symbol: "qt",
20
+ plural: "quarts",
21
+ aliases: ["quart"],
22
+ });
23
+ exports.usPint = exports.volume.unit("usPint", 0.473176473, {
24
+ symbol: "pt",
25
+ plural: "pints",
26
+ aliases: ["pint"],
27
+ });
28
+ exports.usGill = exports.volume.unit("usGill", 0.11829411825, { plural: "gills", aliases: ["gill"] });
29
+ exports.usFluidOunce = exports.volume.unit("usFluidOunce", 0.0295735295625, {
30
+ symbol: "floz",
31
+ plural: "fluidOunces",
32
+ aliases: ["fluidOunce"],
33
+ });
34
+ exports.imperialGallon = exports.volume.unit("imperialGallon", 4.54609, {
35
+ symbol: "gal",
36
+ plural: "gallons",
37
+ aliases: ["gallon"],
38
+ });
39
+ exports.imperialQuart = exports.volume.unit("imperialQuart", 1.1365225, {
40
+ symbol: "qt",
41
+ plural: "quarts",
42
+ aliases: ["quart"],
43
+ });
44
+ exports.imperialPint = exports.volume.unit("imperialPint", 0.56826125, {
45
+ symbol: "pt",
46
+ plural: "pints",
47
+ aliases: ["pint"],
48
+ });
49
+ exports.imperialGill = exports.volume.unit("imperialGill", 0.1420653125, {
50
+ plural: "gills",
51
+ aliases: ["gill"],
52
+ });
53
+ exports.imperialFluidOunce = exports.volume.unit("imperialFluidOunce", 0.0284130625, {
54
+ symbol: "floz",
55
+ plural: "fluidOunces",
56
+ aliases: ["fluidOunce"],
57
+ });
31
58
  // US-only cooking measures (no competing imperial unit, so left unprefixed).
32
- exports.cup = exports.volume.unit("cup", 0.2365882365, ["cups"]);
33
- exports.tablespoon = exports.volume.unit("tablespoon", 0.01478676478125, ["tbsp", "tablespoons"]);
34
- exports.teaspoon = exports.volume.unit("teaspoon", 0.00492892159375, ["tsp", "teaspoons"]);
59
+ exports.cup = exports.volume.unit("cup", 0.2365882365, { plural: "cups" });
60
+ exports.tablespoon = exports.volume.unit("tablespoon", 0.01478676478125, {
61
+ symbol: "tbsp",
62
+ plural: "tablespoons",
63
+ });
64
+ exports.teaspoon = exports.volume.unit("teaspoon", 0.00492892159375, {
65
+ symbol: "tsp",
66
+ plural: "teaspoons",
67
+ });
35
68
  /** Every SI-prefixed liter (milliliter, centiliter, kiloliter, …), keyed by name. */
36
- exports.metricVolume = (0, definePrefixed_1.definePrefixed)(exports.volume, { name: "liter", symbol: "L" });
69
+ exports.metricVolume = (0, definePrefixed_1.definePrefixed)(exports.volume, exports.liter);
37
70
  exports.kiloliter = exports.metricVolume.kiloliter, exports.hectoliter = exports.metricVolume.hectoliter, exports.decaliter = exports.metricVolume.decaliter, exports.deciliter = exports.metricVolume.deciliter, exports.centiliter = exports.metricVolume.centiliter, exports.milliliter = exports.metricVolume.milliliter;
@@ -1,4 +1,4 @@
1
- import { Rational } from "../lib/Rational";
1
+ import { Rational } from "./Rational";
2
2
  import { Unit } from "./Unit";
3
3
  /** A linear unit with an additive offset (e.g. temperature scales). */
4
4
  export interface AffineSpec {
@@ -16,6 +16,15 @@ export interface CustomSpec {
16
16
  toBase: (value: number) => number;
17
17
  fromBase: (value: number) => number;
18
18
  }
19
+ /** Optional descriptors for a unit: its symbol, plural, and extra parse aliases. */
20
+ export interface UnitDef {
21
+ /** Canonical symbol, e.g. `"g"`, `"km"`, `"°C"`. Also registered for parsing. */
22
+ symbol?: string;
23
+ /** Plural name, e.g. `"grams"`. Also registered for parsing. */
24
+ plural?: string;
25
+ /** Additional names this unit parses from (beyond name, symbol, and plural). */
26
+ aliases?: string[];
27
+ }
19
28
  /**
20
29
  * A dimension is a single *kind* of measurable quantity (length, volume, mass,
21
30
  * temperature, …). It owns one canonical **base unit** that every other unit in
@@ -48,15 +57,15 @@ export declare class Dimension {
48
57
  baseUnit?: Unit;
49
58
  constructor(name: string);
50
59
  /** Define the canonical base unit (identity transform). */
51
- base(name: string, aliases?: string[]): Unit;
60
+ base(name: string, def?: UnitDef): Unit;
52
61
  /**
53
62
  * Define a linear unit. `scale` is how many base units make up one of this
54
63
  * unit (e.g. a kilometer is `1000` meters). Pass a {@link Rational} for a
55
64
  * scale a decimal cannot represent exactly.
56
65
  */
57
- unit(name: string, scale: number | Rational, aliases?: string[]): Unit;
66
+ unit(name: string, scale: number | Rational, def?: UnitDef): Unit;
58
67
  /** Define an affine unit (scale plus additive offset, e.g. °C against K). */
59
- affine(name: string, { scale, offset }: AffineSpec, aliases?: string[]): Unit;
68
+ affine(name: string, { scale, offset }: AffineSpec, def?: UnitDef): Unit;
60
69
  /**
61
70
  * Define a non-linear unit from an arbitrary, hand-written inverse transform
62
71
  * pair. Reserve this for units that genuinely cannot be expressed as `value *
@@ -64,7 +73,7 @@ export declare class Dimension {
64
73
  * affine units should use {@link unit} / {@link affine} so conversions stay
65
74
  * exact.
66
75
  */
67
- custom(name: string, { toBase, fromBase }: CustomSpec, aliases?: string[]): Unit;
76
+ custom(name: string, { toBase, fromBase }: CustomSpec, def?: UnitDef): Unit;
68
77
  /** Convert a `number` value between two units of this dimension. */
69
78
  convert(value: number, from: Unit, to: Unit): number;
70
79
  /**
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Dimension = void 0;
4
4
  const InvalidConversionError_1 = require("../errors/InvalidConversionError");
5
- const Rational_1 = require("../lib/Rational");
5
+ const Rational_1 = require("./Rational");
6
6
  const Unit_1 = require("./Unit");
7
7
  /** The rational `0`. */
8
8
  const zero = new Rational_1.Rational(0n);
@@ -39,8 +39,8 @@ class Dimension {
39
39
  this.index = new Map();
40
40
  }
41
41
  /** Define the canonical base unit (identity transform). */
42
- base(name, aliases = []) {
43
- const unit = this.defineLinear(name, { scale: one, offset: zero }, aliases);
42
+ base(name, def = {}) {
43
+ const unit = this.defineLinear(name, { scale: one, offset: zero }, def);
44
44
  this.baseUnit = unit;
45
45
  return unit;
46
46
  }
@@ -49,12 +49,12 @@ class Dimension {
49
49
  * unit (e.g. a kilometer is `1000` meters). Pass a {@link Rational} for a
50
50
  * scale a decimal cannot represent exactly.
51
51
  */
52
- unit(name, scale, aliases = []) {
53
- return this.defineLinear(name, { scale: Rational_1.Rational.from(scale), offset: zero }, aliases);
52
+ unit(name, scale, def = {}) {
53
+ return this.defineLinear(name, { scale: Rational_1.Rational.from(scale), offset: zero }, def);
54
54
  }
55
55
  /** Define an affine unit (scale plus additive offset, e.g. °C against K). */
56
- affine(name, { scale, offset }, aliases = []) {
57
- return this.defineLinear(name, { scale: Rational_1.Rational.from(scale), offset: Rational_1.Rational.from(offset) }, aliases);
56
+ affine(name, { scale, offset }, def = {}) {
57
+ return this.defineLinear(name, { scale: Rational_1.Rational.from(scale), offset: Rational_1.Rational.from(offset) }, def);
58
58
  }
59
59
  /**
60
60
  * Define a non-linear unit from an arbitrary, hand-written inverse transform
@@ -63,8 +63,8 @@ class Dimension {
63
63
  * affine units should use {@link unit} / {@link affine} so conversions stay
64
64
  * exact.
65
65
  */
66
- custom(name, { toBase, fromBase }, aliases = []) {
67
- return this.define(name, aliases, { toBase, fromBase });
66
+ custom(name, { toBase, fromBase }, def = {}) {
67
+ return this.define(name, def, { toBase, fromBase });
68
68
  }
69
69
  /** Convert a `number` value between two units of this dimension. */
70
70
  convert(value, from, to) {
@@ -99,20 +99,22 @@ class Dimension {
99
99
  return this.units.has(unit);
100
100
  }
101
101
  /** Define a linear / affine unit from its exact rational transform. */
102
- defineLinear(name, linear, aliases) {
103
- return this.define(name, aliases, { linear });
102
+ defineLinear(name, linear, def) {
103
+ return this.define(name, def, { linear });
104
104
  }
105
- define(name, aliases, transform) {
105
+ define(name, { symbol, plural, aliases = [] }, transform) {
106
106
  for (const existing of this.units) {
107
107
  if (existing.name === name) {
108
108
  throw new Error(`Duplicate unit name "${name}" in dimension "${this.name}"`);
109
109
  }
110
110
  }
111
- const unit = new Unit_1.Unit({ name, dimension: this, ...transform });
111
+ const unit = new Unit_1.Unit({ name, dimension: this, symbol, plural, ...transform });
112
112
  this.units.add(unit);
113
- this.register(name, unit);
114
- for (const alias of aliases) {
115
- this.register(alias, unit);
113
+ // Register every label this unit can be parsed from, de-duplicated so a unit
114
+ // never appears twice among a token's candidates (e.g. if symbol === name).
115
+ const tokens = new Set([name, symbol, plural, ...aliases].filter((t) => !!t));
116
+ for (const token of tokens) {
117
+ this.register(token, unit);
116
118
  }
117
119
  return unit;
118
120
  }
@@ -7,6 +7,45 @@ export interface ParseOptions {
7
7
  /** Preferred measurement system, used only to break ties on shared aliases. */
8
8
  prefer?: MeasurementSystem;
9
9
  }
10
+ /** Options for {@link Quantity.format}. */
11
+ export interface FormatOptions {
12
+ /**
13
+ * Which label to render after the magnitude:
14
+ * - `"auto"` (default) — singular `name` when the magnitude is exactly ±1,
15
+ * otherwise the `plural`.
16
+ * - `"name"` — always the singular name.
17
+ * - `"plural"` — always the plural.
18
+ * - `"symbol"` — the unit's symbol.
19
+ *
20
+ * `plural`/`symbol` fall back to the unit's `name` when that field is unset.
21
+ * Labels are English/canonical; localize the *magnitude* via {@link locale} /
22
+ * {@link numberFormat}.
23
+ */
24
+ unit?: "auto" | "name" | "plural" | "symbol";
25
+ /**
26
+ * BCP 47 locale(s) used to render the magnitude (e.g. `"de-DE"`, `["fr", "en"]`).
27
+ * Passed straight to `Number.prototype.toLocaleString`. When omitted, the
28
+ * runtime's default locale is used.
29
+ */
30
+ locale?: string | string[];
31
+ /**
32
+ * `Intl.NumberFormat` options for the magnitude — precision
33
+ * (`minimumFractionDigits` / `maximumFractionDigits`), `style: "currency"`,
34
+ * grouping, and so on. Passed straight to `Number.prototype.toLocaleString`.
35
+ */
36
+ numberFormat?: Intl.NumberFormatOptions;
37
+ }
38
+ /**
39
+ * The rendered pieces of a formatted quantity, as returned by
40
+ * {@link Quantity.formatParts}. Each is already a finished string; the caller
41
+ * decides how to assemble them (a plain join, JSX, a template, …).
42
+ */
43
+ export interface FormattedParts {
44
+ /** The locale-formatted magnitude, e.g. `"1.234,5"`. */
45
+ magnitude: string;
46
+ /** The chosen unit label, e.g. `"kilometers"`, `"km"`. */
47
+ unit: string;
48
+ }
10
49
  /** A magnitude paired with a unit (e.g. `5` `kilometer`). */
11
50
  export declare class Quantity {
12
51
  readonly unit: Unit;
@@ -30,6 +69,37 @@ export declare class Quantity {
30
69
  private inRational;
31
70
  /** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
32
71
  toString(): string;
72
+ /**
73
+ * Render as `"<magnitude> <label>"`, choosing the label per `options.unit`
74
+ * (default `"auto"`: magnitude-aware singular/plural). Unlike {@link toString},
75
+ * this can use the unit's symbol or plural — e.g. `"5 grams"`, `"5 g"`,
76
+ * `"1 gram"`. Pass `locale` / `numberFormat` to localize the magnitude via
77
+ * `toLocaleString` — e.g. `format({ locale: "de-DE" })` → `"1.234,5 meters"`,
78
+ * or `format({ numberFormat: { maximumFractionDigits: 2 } })` for precision.
79
+ *
80
+ * For non-string output (e.g. JSX), use {@link formatParts} and assemble the
81
+ * pieces yourself.
82
+ */
83
+ format(options?: FormatOptions): string;
84
+ /**
85
+ * Like {@link format}, but returns the rendered magnitude and label as
86
+ * separate strings instead of joining them, so the caller controls the
87
+ * assembly. Useful when a single string won't do — e.g. styling the magnitude
88
+ * in a React component:
89
+ *
90
+ * ```tsx
91
+ * const { magnitude, unit } = q.formatParts({ locale: "de-DE" });
92
+ * return <><b>{magnitude}</b> {unit}</>;
93
+ * ```
94
+ */
95
+ formatParts(options?: FormatOptions): FormattedParts;
96
+ /**
97
+ * Render the magnitude via `toLocaleString`. With no `locale`/`numberFormat`
98
+ * it uses the runtime's default locale; supply either for locale- and
99
+ * precision-aware formatting.
100
+ */
101
+ private formatMagnitude;
102
+ private formatLabel;
33
103
  /**
34
104
  * Add another quantity, returned in *this* quantity's unit. The other operand
35
105
  * is converted into this unit first, so the two may use different units of the
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Quantity = void 0;
4
+ const assert_never_1 = require("assert-never");
4
5
  const AmbiguousUnitError_1 = require("../errors/AmbiguousUnitError");
5
6
  const UnknownUnitError_1 = require("../errors/UnknownUnitError");
6
7
  const scaleOf_1 = require("../utils/scaleOf");
@@ -31,6 +32,58 @@ class Quantity {
31
32
  toString() {
32
33
  return `${this.magnitude} ${this.unit.name}`;
33
34
  }
35
+ /**
36
+ * Render as `"<magnitude> <label>"`, choosing the label per `options.unit`
37
+ * (default `"auto"`: magnitude-aware singular/plural). Unlike {@link toString},
38
+ * this can use the unit's symbol or plural — e.g. `"5 grams"`, `"5 g"`,
39
+ * `"1 gram"`. Pass `locale` / `numberFormat` to localize the magnitude via
40
+ * `toLocaleString` — e.g. `format({ locale: "de-DE" })` → `"1.234,5 meters"`,
41
+ * or `format({ numberFormat: { maximumFractionDigits: 2 } })` for precision.
42
+ *
43
+ * For non-string output (e.g. JSX), use {@link formatParts} and assemble the
44
+ * pieces yourself.
45
+ */
46
+ format(options = {}) {
47
+ const { magnitude, unit } = this.formatParts(options);
48
+ return `${magnitude} ${unit}`;
49
+ }
50
+ /**
51
+ * Like {@link format}, but returns the rendered magnitude and label as
52
+ * separate strings instead of joining them, so the caller controls the
53
+ * assembly. Useful when a single string won't do — e.g. styling the magnitude
54
+ * in a React component:
55
+ *
56
+ * ```tsx
57
+ * const { magnitude, unit } = q.formatParts({ locale: "de-DE" });
58
+ * return <><b>{magnitude}</b> {unit}</>;
59
+ * ```
60
+ */
61
+ formatParts(options = {}) {
62
+ return { magnitude: this.formatMagnitude(options), unit: this.formatLabel(options) };
63
+ }
64
+ /**
65
+ * Render the magnitude via `toLocaleString`. With no `locale`/`numberFormat`
66
+ * it uses the runtime's default locale; supply either for locale- and
67
+ * precision-aware formatting.
68
+ */
69
+ formatMagnitude({ locale, numberFormat }) {
70
+ return this.magnitude.toLocaleString(locale, numberFormat);
71
+ }
72
+ formatLabel({ unit = "auto" }) {
73
+ const { name, symbol, plural } = this.unit;
74
+ switch (unit) {
75
+ case "symbol":
76
+ return symbol ?? name;
77
+ case "name":
78
+ return name;
79
+ case "plural":
80
+ return plural ?? name;
81
+ case "auto":
82
+ return Math.abs(this.magnitude) === 1 ? name : (plural ?? name);
83
+ default:
84
+ return (0, assert_never_1.assertNever)(unit);
85
+ }
86
+ }
34
87
  /**
35
88
  * Add another quantity, returned in *this* quantity's unit. The other operand
36
89
  * is converted into this unit first, so the two may use different units of the
@@ -16,7 +16,7 @@ export declare class Rational {
16
16
  /**
17
17
  * Build a rational from integer numerator and denominator. Pass exact ratios
18
18
  * the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
19
- * For a decimal value, use {@link Rational.fromNumber} instead.
19
+ * For a decimal value, use {@link Rational.from} instead.
20
20
  */
21
21
  constructor(numerator: bigint | number, denominator?: bigint | number);
22
22
  /** Coerce a `number | Rational` to a `Rational`, parsing numbers as decimals. */
@@ -17,7 +17,7 @@ class Rational {
17
17
  /**
18
18
  * Build a rational from integer numerator and denominator. Pass exact ratios
19
19
  * the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
20
- * For a decimal value, use {@link Rational.fromNumber} instead.
20
+ * For a decimal value, use {@link Rational.from} instead.
21
21
  */
22
22
  constructor(numerator, denominator = 1n) {
23
23
  let n = toBigInt(numerator);
@@ -9,21 +9,23 @@ export interface LinearTransform {
9
9
  scale: Rational;
10
10
  offset: Rational;
11
11
  }
12
- interface BaseUnitOptions {
12
+ export interface BaseUnitOptions {
13
13
  name: string;
14
14
  dimension: Dimension;
15
+ /** Canonical symbol, e.g. `"g"`, `"km"`, `"°C"` (optional). */
16
+ symbol?: string;
17
+ /** Plural name, e.g. `"grams"`, `"kilometers"` (optional). */
18
+ plural?: string;
15
19
  }
16
- interface LinearConversionOptions {
20
+ export type UnitConversionOptions = {
17
21
  /** Exact transform for linear / affine units (the common case). */
18
22
  linear: LinearTransform;
19
- }
20
- interface CustomConversionOptions {
23
+ } | {
21
24
  /** Hand-written transform for non-linear units; mutually exclusive with `linear`. */
22
25
  toBase: (value: number) => number;
23
26
  fromBase: (value: number) => number;
24
- }
25
- export type UnitConversionOptions = LinearConversionOptions | CustomConversionOptions;
26
- type UnitOptions = BaseUnitOptions & UnitConversionOptions;
27
+ };
28
+ export type UnitOptions = BaseUnitOptions & UnitConversionOptions;
27
29
  /**
28
30
  * A single unit of measurement (e.g. "meter", "celsius").
29
31
  *
@@ -44,8 +46,10 @@ type UnitOptions = BaseUnitOptions & UnitConversionOptions;
44
46
  * {@link MeasurementSystem}s (metric/imperial/…); that membership lives on the
45
47
  * measurement systems, not here, so a `Unit` stays a lean descriptor.
46
48
  *
47
- * Names and aliases live solely in the dimension's lookup index; they are
48
- * declared once when the unit is defined.
49
+ * Parsing aliases live in the dimension's lookup index. A unit additionally
50
+ * carries its canonical {@link symbol} and {@link plural} as first-class data
51
+ * so callers can choose how to render it (see {@link Quantity.format}); both are
52
+ * optional and English-centric — real localization is left to the consumer.
49
53
  *
50
54
  * Units are normally created through a {@link Dimension}'s builder methods
51
55
  * (`base`, `unit`, `affine`, `custom`) rather than constructed directly.
@@ -53,8 +57,12 @@ type UnitOptions = BaseUnitOptions & UnitConversionOptions;
53
57
  export declare class Unit {
54
58
  readonly name: string;
55
59
  readonly dimension: Dimension;
60
+ /** Canonical symbol, e.g. `"g"`, `"km"`, `"°C"` (optional). */
61
+ readonly symbol?: string;
62
+ /** Plural name, e.g. `"grams"`, `"kilometers"` (optional). */
63
+ readonly plural?: string;
56
64
  private readonly conversion;
57
- constructor({ name, dimension, ...conversionOptions }: UnitOptions);
65
+ constructor({ name, dimension, symbol, plural, ...conversionOptions }: UnitOptions);
58
66
  /**
59
67
  * Exact affine transform to the base unit, present for all but non-linear
60
68
  * units. When both ends of a conversion have one, {@link Dimension.convert}
@@ -66,4 +74,3 @@ export declare class Unit {
66
74
  /** Convert a value in the dimension's base unit to this unit. */
67
75
  fromBase(value: number): number;
68
76
  }
69
- export {};
package/dist/lib/Unit.js CHANGED
@@ -21,16 +21,20 @@ exports.Unit = void 0;
21
21
  * {@link MeasurementSystem}s (metric/imperial/…); that membership lives on the
22
22
  * measurement systems, not here, so a `Unit` stays a lean descriptor.
23
23
  *
24
- * Names and aliases live solely in the dimension's lookup index; they are
25
- * declared once when the unit is defined.
24
+ * Parsing aliases live in the dimension's lookup index. A unit additionally
25
+ * carries its canonical {@link symbol} and {@link plural} as first-class data
26
+ * so callers can choose how to render it (see {@link Quantity.format}); both are
27
+ * optional and English-centric — real localization is left to the consumer.
26
28
  *
27
29
  * Units are normally created through a {@link Dimension}'s builder methods
28
30
  * (`base`, `unit`, `affine`, `custom`) rather than constructed directly.
29
31
  */
30
32
  class Unit {
31
- constructor({ name, dimension, ...conversionOptions }) {
33
+ constructor({ name, dimension, symbol, plural, ...conversionOptions }) {
32
34
  this.name = name;
33
35
  this.dimension = dimension;
36
+ this.symbol = symbol;
37
+ this.plural = plural;
34
38
  this.conversion = conversionOptions;
35
39
  }
36
40
  /**
@@ -1,5 +1,4 @@
1
1
  import type { Dimension } from "../lib/Dimension";
2
- import { Rational } from "../lib/Rational";
3
2
  import type { Unit } from "../lib/Unit";
4
3
  /** A metric (SI) prefix: a name, symbol, and power-of-ten factor. */
5
4
  export interface SiPrefix {
@@ -11,20 +10,14 @@ export interface SiPrefix {
11
10
  export declare const SI_PREFIXES: readonly SiPrefix[];
12
11
  /** SI prefixes for fractions only (deci and smaller) — for units like seconds. */
13
12
  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
13
  /**
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).
14
+ * Define metric-prefixed variants of a `reference` unit on its dimension. Each
15
+ * variant is named `${prefix}${reference.name}` (e.g. "kilometer"), scaled by
16
+ * `prefix.factor` relative to the reference (its own scale is read from the unit
17
+ * via `scaleOf`, so prefixing a non-base unit like the watt-hour works
18
+ * automatically). Each variant carries a generated `symbol`
19
+ * (`${prefix.symbol}${reference.symbol}`, when the reference has a symbol) and a
20
+ * `plural` (`${name}s`), plus an ASCII "u" alias for micro.
28
21
  *
29
22
  * Prefixes whose generated name already exists on the dimension are skipped, so
30
23
  * a base like "kilogram" is left intact when prefixing "gram".
@@ -32,4 +25,4 @@ export interface PrefixReference {
32
25
  * Returns the created units keyed by name, for spreading into a
33
26
  * {@link MeasurementSystem} or destructuring into named exports.
34
27
  */
35
- export declare function definePrefixed(dimension: Dimension, reference: PrefixReference, prefixes?: readonly SiPrefix[]): Record<string, Unit>;
28
+ export declare function definePrefixed(dimension: Dimension, reference: Unit, prefixes?: readonly SiPrefix[]): Record<string, Unit>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SI_SUBMULTIPLE_PREFIXES = exports.SI_PREFIXES = void 0;
4
4
  exports.definePrefixed = definePrefixed;
5
5
  const Rational_1 = require("../lib/Rational");
6
+ const scaleOf_1 = require("./scaleOf");
6
7
  /** The full set of SI prefixes, yotta (1e24) down to yocto (1e-24). */
7
8
  exports.SI_PREFIXES = [
8
9
  { name: "yotta", symbol: "Y", factor: 1e24 },
@@ -29,10 +30,13 @@ exports.SI_PREFIXES = [
29
30
  /** SI prefixes for fractions only (deci and smaller) — for units like seconds. */
30
31
  exports.SI_SUBMULTIPLE_PREFIXES = exports.SI_PREFIXES.filter((prefix) => prefix.factor < 1);
31
32
  /**
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).
33
+ * Define metric-prefixed variants of a `reference` unit on its dimension. Each
34
+ * variant is named `${prefix}${reference.name}` (e.g. "kilometer"), scaled by
35
+ * `prefix.factor` relative to the reference (its own scale is read from the unit
36
+ * via `scaleOf`, so prefixing a non-base unit like the watt-hour works
37
+ * automatically). Each variant carries a generated `symbol`
38
+ * (`${prefix.symbol}${reference.symbol}`, when the reference has a symbol) and a
39
+ * `plural` (`${name}s`), plus an ASCII "u" alias for micro.
36
40
  *
37
41
  * Prefixes whose generated name already exists on the dimension are skipped, so
38
42
  * a base like "kilogram" is left intact when prefixing "gram".
@@ -41,21 +45,28 @@ exports.SI_SUBMULTIPLE_PREFIXES = exports.SI_PREFIXES.filter((prefix) => prefix.
41
45
  * {@link MeasurementSystem} or destructuring into named exports.
42
46
  */
43
47
  function definePrefixed(dimension, reference, prefixes = exports.SI_PREFIXES) {
48
+ // Prefer the reference's exact rational scale so the prefixed scale stays
49
+ // exact; fall back to its float scale only for non-linear references.
50
+ const referenceScale = reference.linear
51
+ ? reference.linear.scale
52
+ : Rational_1.Rational.from((0, scaleOf_1.scaleOf)(reference));
44
53
  const units = {};
45
54
  for (const prefix of prefixes) {
46
55
  const name = `${prefix.name}${reference.name}`;
47
56
  if (dimension.get(name)) {
48
57
  continue;
49
58
  }
50
- const aliases = [`${prefix.symbol}${reference.symbol}`, `${name}s`];
51
- if (prefix.name === "micro") {
52
- aliases.push(`u${reference.symbol}`);
53
- }
59
+ const symbol = reference.symbol ? `${prefix.symbol}${reference.symbol}` : undefined;
60
+ const aliases = prefix.name === "micro" && reference.symbol ? [`u${reference.symbol}`] : [];
54
61
  // Multiply as rationals: each factor (a power of ten, or an exact reference
55
62
  // scale) is lossless on its own, but multiplying them as floats can drift
56
63
  // (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);
64
+ const scale = referenceScale.times(Rational_1.Rational.from(prefix.factor));
65
+ units[name] = dimension.unit(name, scale, {
66
+ symbol,
67
+ plural: `${name}s`,
68
+ aliases,
69
+ });
59
70
  }
60
71
  return units;
61
72
  }