measurable 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +129 -27
  3. package/dist/dimensions/angle.js +12 -5
  4. package/dist/dimensions/area.js +37 -16
  5. package/dist/dimensions/data.js +11 -8
  6. package/dist/dimensions/energy.js +18 -11
  7. package/dist/dimensions/force.js +5 -5
  8. package/dist/dimensions/frequency.js +2 -2
  9. package/dist/dimensions/illuminance.js +7 -4
  10. package/dist/dimensions/length.js +6 -6
  11. package/dist/dimensions/luminance.js +5 -8
  12. package/dist/dimensions/luminousIntensity.js +7 -7
  13. package/dist/dimensions/mass.js +12 -8
  14. package/dist/dimensions/power.js +4 -4
  15. package/dist/dimensions/pressure.js +12 -9
  16. package/dist/dimensions/temperature.js +3 -3
  17. package/dist/dimensions/time.js +18 -6
  18. package/dist/dimensions/volume.js +56 -23
  19. package/dist/errors/AmbiguousUnitError.js +1 -2
  20. package/dist/errors/ArgumentError.d.ts +7 -0
  21. package/dist/errors/ArgumentError.js +11 -0
  22. package/dist/errors/DimensionMismatchError.d.ts +13 -0
  23. package/dist/errors/DimensionMismatchError.js +16 -0
  24. package/dist/errors/DuplicateUnitError.d.ts +5 -0
  25. package/dist/errors/DuplicateUnitError.js +10 -0
  26. package/dist/errors/ParseError.d.ts +4 -0
  27. package/dist/errors/ParseError.js +10 -0
  28. package/dist/errors/UnsupportedDimensionError.d.ts +10 -0
  29. package/dist/errors/UnsupportedDimensionError.js +14 -0
  30. package/dist/index.d.ts +5 -1
  31. package/dist/index.js +5 -1
  32. package/dist/lib/Dimension.d.ts +14 -5
  33. package/dist/lib/Dimension.js +22 -19
  34. package/dist/lib/MeasurementSystem.js +4 -15
  35. package/dist/lib/Quantity.d.ts +81 -3
  36. package/dist/lib/Quantity.js +82 -4
  37. package/dist/lib/Rational.d.ts +1 -1
  38. package/dist/lib/Rational.js +6 -5
  39. package/dist/lib/Unit.d.ts +18 -11
  40. package/dist/lib/Unit.js +7 -3
  41. package/dist/utils/definePrefixed.d.ts +8 -15
  42. package/dist/utils/definePrefixed.js +21 -10
  43. package/package.json +8 -3
@@ -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;
@@ -28,12 +67,51 @@ export declare class Quantity {
28
67
  in(target: Unit): number;
29
68
  /** This quantity's exact magnitude expressed in `target`. */
30
69
  private inRational;
70
+ /**
71
+ * Re-express this quantity in the best-fit unit among `units`: the largest
72
+ * unit whose absolute magnitude is still at least 1 (falling back to the
73
+ * smallest unit when even that rounds below 1). Requires at least one unit,
74
+ * and each must belong to this quantity's dimension (else
75
+ * {@link DimensionMismatchError}).
76
+ */
77
+ best(...units: Unit[]): Quantity;
31
78
  /** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
32
79
  toString(): string;
80
+ /**
81
+ * Render as `"<magnitude> <label>"`, choosing the label per `options.unit`
82
+ * (default `"auto"`: magnitude-aware singular/plural). Unlike {@link toString},
83
+ * this can use the unit's symbol or plural — e.g. `"5 grams"`, `"5 g"`,
84
+ * `"1 gram"`. Pass `locale` / `numberFormat` to localize the magnitude via
85
+ * `toLocaleString` — e.g. `format({ locale: "de-DE" })` → `"1.234,5 meters"`,
86
+ * or `format({ numberFormat: { maximumFractionDigits: 2 } })` for precision.
87
+ *
88
+ * For non-string output (e.g. JSX), use {@link formatParts} and assemble the
89
+ * pieces yourself.
90
+ */
91
+ format(options?: FormatOptions): string;
92
+ /**
93
+ * Like {@link format}, but returns the rendered magnitude and label as
94
+ * separate strings instead of joining them, so the caller controls the
95
+ * assembly. Useful when a single string won't do — e.g. styling the magnitude
96
+ * in a React component:
97
+ *
98
+ * ```tsx
99
+ * const { magnitude, unit } = q.formatParts({ locale: "de-DE" });
100
+ * return <><b>{magnitude}</b> {unit}</>;
101
+ * ```
102
+ */
103
+ formatParts(options?: FormatOptions): FormattedParts;
104
+ /**
105
+ * Render the magnitude via `toLocaleString`. With no `locale`/`numberFormat`
106
+ * it uses the runtime's default locale; supply either for locale- and
107
+ * precision-aware formatting.
108
+ */
109
+ private formatMagnitude;
110
+ private formatLabel;
33
111
  /**
34
112
  * Add another quantity, returned in *this* quantity's unit. The other operand
35
113
  * is converted into this unit first, so the two may use different units of the
36
- * same dimension (e.g. `mile.plus(km)`). Throws {@link InvalidConversionError}
114
+ * same dimension (e.g. `mile.plus(km)`). Throws {@link DimensionMismatchError}
37
115
  * if the operands belong to different dimensions.
38
116
  *
39
117
  * Note: for affine units (e.g. temperature) addition is mathematically defined
@@ -51,7 +129,7 @@ export declare class Quantity {
51
129
  * Divide this quantity by `other` of the same dimension, yielding the
52
130
  * dimensionless ratio between them — i.e. how many of `other` fit in this.
53
131
  * Unlike {@link in}, this accounts for `other`'s magnitude, not just its unit.
54
- * Throws {@link InvalidConversionError} across dimensions.
132
+ * Throws {@link DimensionMismatchError} across dimensions.
55
133
  */
56
134
  ratioTo(other: Quantity): number;
57
135
  /** Return this quantity with its magnitude negated. */
@@ -72,7 +150,7 @@ export declare class Quantity {
72
150
  div(divisor: number | Rational): Quantity;
73
151
  /**
74
152
  * Whether this quantity equals `other`, compared in this quantity's unit.
75
- * Throws {@link InvalidConversionError} if the operands belong to different
153
+ * Throws {@link DimensionMismatchError} if the operands belong to different
76
154
  * dimensions. Comparison is exact rational equality, so quantities that are
77
155
  * mathematically equal compare equal even if reaching them involved a
78
156
  * conversion that would have drifted in floating point.
@@ -1,7 +1,10 @@
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");
6
+ const ArgumentError_1 = require("../errors/ArgumentError");
7
+ const ParseError_1 = require("../errors/ParseError");
5
8
  const UnknownUnitError_1 = require("../errors/UnknownUnitError");
6
9
  const scaleOf_1 = require("../utils/scaleOf");
7
10
  const Rational_1 = require("./Rational");
@@ -27,14 +30,89 @@ class Quantity {
27
30
  inRational(target) {
28
31
  return this.unit.dimension.convertRational(this.rational, this.unit, target);
29
32
  }
33
+ /**
34
+ * Re-express this quantity in the best-fit unit among `units`: the largest
35
+ * unit whose absolute magnitude is still at least 1 (falling back to the
36
+ * smallest unit when even that rounds below 1). Requires at least one unit,
37
+ * and each must belong to this quantity's dimension (else
38
+ * {@link DimensionMismatchError}).
39
+ */
40
+ best(...units) {
41
+ const candidates = [...units].sort((a, b) => (0, scaleOf_1.scaleOf)(a) - (0, scaleOf_1.scaleOf)(b));
42
+ if (candidates.length === 0) {
43
+ throw new ArgumentError_1.ArgumentError("Quantity.best requires at least one unit");
44
+ }
45
+ let chosen = candidates[0];
46
+ for (const unit of candidates) {
47
+ if (Math.abs(this.in(unit)) >= 1) {
48
+ chosen = unit;
49
+ }
50
+ else {
51
+ break;
52
+ }
53
+ }
54
+ return this.to(chosen);
55
+ }
30
56
  /** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
31
57
  toString() {
32
58
  return `${this.magnitude} ${this.unit.name}`;
33
59
  }
60
+ /**
61
+ * Render as `"<magnitude> <label>"`, choosing the label per `options.unit`
62
+ * (default `"auto"`: magnitude-aware singular/plural). Unlike {@link toString},
63
+ * this can use the unit's symbol or plural — e.g. `"5 grams"`, `"5 g"`,
64
+ * `"1 gram"`. Pass `locale` / `numberFormat` to localize the magnitude via
65
+ * `toLocaleString` — e.g. `format({ locale: "de-DE" })` → `"1.234,5 meters"`,
66
+ * or `format({ numberFormat: { maximumFractionDigits: 2 } })` for precision.
67
+ *
68
+ * For non-string output (e.g. JSX), use {@link formatParts} and assemble the
69
+ * pieces yourself.
70
+ */
71
+ format(options = {}) {
72
+ const { magnitude, unit } = this.formatParts(options);
73
+ return `${magnitude} ${unit}`;
74
+ }
75
+ /**
76
+ * Like {@link format}, but returns the rendered magnitude and label as
77
+ * separate strings instead of joining them, so the caller controls the
78
+ * assembly. Useful when a single string won't do — e.g. styling the magnitude
79
+ * in a React component:
80
+ *
81
+ * ```tsx
82
+ * const { magnitude, unit } = q.formatParts({ locale: "de-DE" });
83
+ * return <><b>{magnitude}</b> {unit}</>;
84
+ * ```
85
+ */
86
+ formatParts(options = {}) {
87
+ return { magnitude: this.formatMagnitude(options), unit: this.formatLabel(options) };
88
+ }
89
+ /**
90
+ * Render the magnitude via `toLocaleString`. With no `locale`/`numberFormat`
91
+ * it uses the runtime's default locale; supply either for locale- and
92
+ * precision-aware formatting.
93
+ */
94
+ formatMagnitude({ locale, numberFormat }) {
95
+ return this.magnitude.toLocaleString(locale, numberFormat);
96
+ }
97
+ formatLabel({ unit = "auto" }) {
98
+ const { name, symbol, plural } = this.unit;
99
+ switch (unit) {
100
+ case "symbol":
101
+ return symbol ?? name;
102
+ case "name":
103
+ return name;
104
+ case "plural":
105
+ return plural ?? name;
106
+ case "auto":
107
+ return Math.abs(this.magnitude) === 1 ? name : (plural ?? name);
108
+ default:
109
+ return (0, assert_never_1.assertNever)(unit);
110
+ }
111
+ }
34
112
  /**
35
113
  * Add another quantity, returned in *this* quantity's unit. The other operand
36
114
  * is converted into this unit first, so the two may use different units of the
37
- * same dimension (e.g. `mile.plus(km)`). Throws {@link InvalidConversionError}
115
+ * same dimension (e.g. `mile.plus(km)`). Throws {@link DimensionMismatchError}
38
116
  * if the operands belong to different dimensions.
39
117
  *
40
118
  * Note: for affine units (e.g. temperature) addition is mathematically defined
@@ -60,7 +138,7 @@ class Quantity {
60
138
  * Divide this quantity by `other` of the same dimension, yielding the
61
139
  * dimensionless ratio between them — i.e. how many of `other` fit in this.
62
140
  * Unlike {@link in}, this accounts for `other`'s magnitude, not just its unit.
63
- * Throws {@link InvalidConversionError} across dimensions.
141
+ * Throws {@link DimensionMismatchError} across dimensions.
64
142
  */
65
143
  ratioTo(other) {
66
144
  return this.rational.dividedBy(other.inRational(this.unit)).toNumber();
@@ -106,7 +184,7 @@ class Quantity {
106
184
  }
107
185
  /**
108
186
  * Whether this quantity equals `other`, compared in this quantity's unit.
109
- * Throws {@link InvalidConversionError} if the operands belong to different
187
+ * Throws {@link DimensionMismatchError} if the operands belong to different
110
188
  * dimensions. Comparison is exact rational equality, so quantities that are
111
189
  * mathematically equal compare equal even if reaching them involved a
112
190
  * conversion that would have drifted in floating point.
@@ -204,7 +282,7 @@ class Quantity {
204
282
  }
205
283
  }
206
284
  if (!total || !finest) {
207
- throw new Error(`Could not parse a quantity from "${str}"`);
285
+ throw new ParseError_1.ParseError(str);
208
286
  }
209
287
  return total.to(finest);
210
288
  }
@@ -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. */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Rational = void 0;
4
+ const ArgumentError_1 = require("../errors/ArgumentError");
4
5
  /**
5
6
  * An exact rational number (`n / d`) used to make conversions between linear
6
7
  * and affine units lossless. Such conversions are inherently rational — a foot
@@ -17,13 +18,13 @@ class Rational {
17
18
  /**
18
19
  * Build a rational from integer numerator and denominator. Pass exact ratios
19
20
  * the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
20
- * For a decimal value, use {@link Rational.fromNumber} instead.
21
+ * For a decimal value, use {@link Rational.from} instead.
21
22
  */
22
23
  constructor(numerator, denominator = 1n) {
23
24
  let n = toBigInt(numerator);
24
25
  let d = toBigInt(denominator);
25
26
  if (d === 0n) {
26
- throw new Error("Rational denominator cannot be zero");
27
+ throw new ArgumentError_1.ArgumentError("Rational denominator cannot be zero");
27
28
  }
28
29
  if (d < 0n) {
29
30
  n = -n;
@@ -48,11 +49,11 @@ class Rational {
48
49
  */
49
50
  static fromNumber(value) {
50
51
  if (!Number.isFinite(value)) {
51
- throw new Error(`Cannot derive a rational from ${value}`);
52
+ throw new ArgumentError_1.ArgumentError(`Cannot derive a rational from ${value}`);
52
53
  }
53
54
  const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(value.toString());
54
55
  if (!match) {
55
- throw new Error(`Cannot derive a rational from ${value}`);
56
+ throw new ArgumentError_1.ArgumentError(`Cannot derive a rational from ${value}`);
56
57
  }
57
58
  const [, sign, intPart, fracPart = "", expPart] = match;
58
59
  let n = BigInt(intPart + fracPart);
@@ -168,7 +169,7 @@ function toBigInt(value) {
168
169
  return value;
169
170
  }
170
171
  if (!Number.isInteger(value)) {
171
- throw new Error(`Rational numerator and denominator must be integers; got ${value}`);
172
+ throw new ArgumentError_1.ArgumentError(`Rational numerator and denominator must be integers; got ${value}`);
172
173
  }
173
174
  return BigInt(value);
174
175
  }
@@ -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
  }