@ultimat3/money 24.0.0 → 25.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/CLAUDE.md CHANGED
@@ -49,7 +49,7 @@ the wide shape and answer with the row type.
49
49
  `scaleOf` still answer for a *currency*, which is a different question.
50
50
  - **A stated currency is an ASSERTION, never a fallback.** `sum(amounts, currency)` used its
51
51
  second argument only when the list was empty and ignored it entirely once a first addend existed:
52
- `sum([money(1, 'EUR')], 'USD')` answered `{ minor: 1, currency: 'EUR' }`, so a caller who wrote
52
+ `sum([fromMinor(1, 'EUR')], 'USD')` answered `{ minor: 1, currency: 'EUR' }`, so a caller who wrote
53
53
  down USD received EUR with nothing refused — in the one entry point of a file whose header is
54
54
  "Integer arithmetic that refuses to mix currencies". A stated currency the first addend
55
55
  contradicts is `X_CURRENCY_MISMATCH`.
@@ -60,13 +60,13 @@ the wide shape and answer with the row type.
60
60
  - **Lossy narrowing names its mode at the call.** `rescale(m, 2)` throws rather than drop a
61
61
  non-zero digit; `rescale(m, 2, 'half-up')` is the same rule `fromDecimal` applies to excess
62
62
  precision. A narrowing that loses nothing needs no mode — nothing is being decided.
63
- - **`money()` is the only place the canonical form is decided.** It drops a `scale` equal to the
64
- currency's exponent — only equal, so a deliberately *coarser* scale (`money(5, 'USD', 0)`, whole
63
+ - **`fromMinor()` is the only place the canonical form is decided.** It drops a `scale` equal to the
64
+ currency's exponent — only equal, so a deliberately *coarser* scale (`fromMinor(5, 'USD', 0)`, whole
65
65
  dollars) is kept exactly as a finer one is. Every constructor, every arithmetic result and every
66
66
  allocation part therefore agree on one encoding without any of them repeating the rule.
67
67
  - **A widened value that will not fit is a scale error, not a fractional-minor one.** `add`,
68
68
  `subtract` and `rescale` convert through `toMinor`, which throws `X_MONEY_SCALE_INVALID` naming
69
- the finest scale that fits. Letting `money()` refuse the raw number reported a fractional minor
69
+ the finest scale that fits. Letting `fromMinor()` refuse the raw number reported a fractional minor
70
70
  nobody wrote, with a `fromDecimal` fix line that threw the same error again.
71
71
  - Never combine currencies without `convert()` first.
72
72
  - Never round without naming a `RoundingMode` in the call or accepting the stated default.
@@ -85,7 +85,7 @@ the wide shape and answer with the row type.
85
85
  of a value that names none; a value that names one keeps it, because narrowing $0.000002 to
86
86
  EUR's two decimals is the 10,000x reinterpretation `scale` was added to prevent. Same rule as
87
87
  `multiply` and `divide`, which already kept theirs.
88
- - **`money()` normalises `-0` to `0`.** One amount must not have two identities: `JSON.stringify`
88
+ - **`fromMinor()` normalises `-0` to `0`.** One amount must not have two identities: `JSON.stringify`
89
89
  writes `-0` as `0` while `Object.is` and any keyed `Map` see something else, so a refund
90
90
  rounding to nothing produced a value its own wire format cannot reproduce. `roundToInteger`
91
91
  refuses to produce it either — `sign * 0` is the source.
package/README.md CHANGED
@@ -21,14 +21,14 @@ read rather than rounding it. → [Money](https://github.com/developerz-ai/ultim
21
21
  ## Use
22
22
 
23
23
  ```ts
24
- import { add, allocate, formatMoney, fromDecimal, money } from '@ultimat3/money';
24
+ import { add, allocate, formatMoney, fromDecimal, fromMinor } from '@ultimat3/money';
25
25
 
26
26
  const price = fromDecimal('12.99', 'EUR'); // { minor: 1299, currency: 'EUR' }
27
- const total = add(price, money(500, 'EUR')); // 1799
27
+ const total = add(price, fromMinor(500, 'EUR')); // 1799
28
28
  formatMoney(total, 'de-DE'); // "17,99 €"
29
- formatMoney(money(1200, 'JPY'), 'en-US'); // "¥1,200" — 0 decimals
30
- formatMoney(money(1234, 'KWD'), 'en-US'); // "KWD 1.234" — 3 decimals
31
- add(price, money(500, 'USD')); // throws X_CURRENCY_MISMATCH
29
+ formatMoney(fromMinor(1200, 'JPY'), 'en-US'); // "¥1,200" — 0 decimals
30
+ formatMoney(fromMinor(1234, 'KWD'), 'en-US'); // "KWD 1.234" — 3 decimals
31
+ add(price, fromMinor(500, 'USD')); // throws X_CURRENCY_MISMATCH
32
32
  ```
33
33
 
34
34
  ## Minor units are not always cents
@@ -59,7 +59,7 @@ Once, at boot, before the first amount in that currency is built. The rules, eac
59
59
  | Rule | Refusal |
60
60
  |---|---|
61
61
  | three A–Z letters — `Intl` throws a `RangeError` on anything else | `X_CURRENCY_INVALID` |
62
- | a whole exponent from 0 to `MAX_MONEY_SCALE` — there is no safe default, and a silent 2 is the corrupted maths this package exists to prevent | `X_CURRENCY_INVALID` |
62
+ | a whole exponent from 0 to `@ultimat3/schema`'s `MAX_MONEY_SCALE` — there is no safe default, and a silent 2 is the corrupted maths this package exists to prevent | `X_CURRENCY_INVALID` |
63
63
  | a non-empty name | `X_CURRENCY_INVALID` |
64
64
  | one code, one declaration — a second exponent reinterprets every stored amount by a power of ten, and a second name makes `currencyInfo().name` depend on import order. An **identical** re-registration is a no-op, so a module imported twice is not a crash | `X_CURRENCY_REDEFINED` |
65
65
  | a shipped ISO row is not the app's to redefine | `X_CURRENCY_REDEFINED` |
@@ -69,22 +69,22 @@ included. That is why one is a value and the other is a call.
69
69
 
70
70
  ## Sub-cent amounts carry a scale
71
71
 
72
- `money(2, 'USD', 6)` is $0.000002 — `minor` counting 10⁻⁶ instead of the currency's own 10⁻².
72
+ `fromMinor(2, 'USD', 6)` is $0.000002 — `minor` counting 10⁻⁶ instead of the currency's own 10⁻².
73
73
  A value that names no scale means the currency's, which is every amount that already exists, so
74
74
  nothing about `{ minor, currency }` changes: same shape, same JSON, same columns. Only a scale
75
- *equal* to the currency's is dropped, so a deliberately coarser one is kept too: `money(5, 'USD', 0)`
75
+ *equal* to the currency's is dropped, so a deliberately coarser one is kept too: `fromMinor(5, 'USD', 0)`
76
76
  is $5 counted in whole dollars, and `rescale()` produces such values legitimately.
77
77
 
78
78
  ```ts
79
- import { add, fromDecimal, money, moneyScale, rescale } from '@ultimat3/money';
79
+ import { add, fromDecimal, fromMinor, moneyScale, rescale } from '@ultimat3/money';
80
80
 
81
- moneyScale(money(1299, 'EUR')); // 2 — the currency's own
82
- moneyScale(money(2, 'USD', 6)); // 6
83
- rescale(money(80, 'USD'), 8); // $0.80 as 80,000,000 hundred-millionths
84
- rescale(money(1_234_567, 'USD', 6), 2); // throws X_MONEY_NOT_INTEGER — digits would go
85
- rescale(money(1_234_567, 'USD', 6), 2, 'half-up'); // 123¢, the loss named at the call
81
+ moneyScale(fromMinor(1299, 'EUR')); // 2 — the currency's own
82
+ moneyScale(fromMinor(2, 'USD', 6)); // 6
83
+ rescale(fromMinor(80, 'USD'), 8); // $0.80 as 80,000,000 hundred-millionths
84
+ rescale(fromMinor(1_234_567, 'USD', 6), 2); // throws X_MONEY_NOT_INTEGER — digits would go
85
+ rescale(fromMinor(1_234_567, 'USD', 6), 2, 'half-up'); // 123¢, the loss named at the call
86
86
  fromDecimal('0.000002', 'USD', { scale: 6 });
87
- add(money(1, 'USD'), money(2, 'USD', 6)); // meets at scale 6: 10002, nothing lost
87
+ add(fromMinor(1, 'USD'), fromMinor(2, 'USD', 6)); // meets at scale 6: 10002, nothing lost
88
88
  ```
89
89
 
90
90
  Arithmetic normalises to the *finer* of two scales, never the coarser — adding a sub-cent fee to
@@ -101,7 +101,7 @@ fiction. The alternative was a second money type.
101
101
 
102
102
  ## Allocation
103
103
 
104
- `allocate(money(100, 'USD'), 3)` → `34, 33, 33`. Largest-remainder split: floor every part,
104
+ `allocate(fromMinor(100, 'USD'), 3)` → `34, 33, 33`. Largest-remainder split: floor every part,
105
105
  then hand out the leftover units one at a time, biggest fractional remainder first.
106
106
  `round(100 / 3)` either loses a cent or invents one, and an invoice that does that fails
107
107
  reconciliation forever. `allocateByRatios` does the same for revenue shares and line splits.
@@ -128,7 +128,7 @@ scales by that when it is there. It is how a derived direction stays exact: a ta
128
128
  double `1 / 0.92`, whose own decimal spelling rounds a large amount one minor unit low. `rate`
129
129
  stays the readable number the audit trail records.
130
130
 
131
- `convert` preserves the amount's own `scale`. `convert(money(2, 'USD', 6), 'EUR', parity)` is
131
+ `convert` preserves the amount's own `scale`. `convert(fromMinor(2, 'USD', 6), 'EUR', parity)` is
132
132
  €0.000002, not €0.02 — the target currency's minor unit decides nothing about a value that
133
133
  already carries its own precision. `convertWith` on a same-currency pair stamps `at` from an
134
134
  injected `Clock` (`{ clock }`, default `systemClock`) or from an explicit `{ at }`, never from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/money",
3
- "version": "24.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "Integer minor units with an attached currency: arithmetic, allocation, rounding, Intl formatting",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,14 +28,14 @@
28
28
  "LICENSE"
29
29
  ],
30
30
  "engines": {
31
- "bun": ">=1.4.0"
31
+ "bun": ">=1.4.2"
32
32
  },
33
33
  "scripts": {
34
34
  "typecheck": "tsc --noEmit -p tsconfig.json",
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@ultimat3/core": "24.0.0",
39
- "@ultimat3/schema": "24.0.0"
38
+ "@ultimat3/core": "25.0.0",
39
+ "@ultimat3/schema": "25.0.0"
40
40
  }
41
41
  }
package/src/allocate.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import { assertSameCurrency } from './arithmetic';
12
12
  import { allocationInvalid } from './errors';
13
13
  import { factorFraction } from './factor';
14
- import { formatMoneyDebug, type Money, money } from './money';
14
+ import { formatMoneyDebug, fromMinor, type Money } from './money';
15
15
  import { minorAt, moneyScale } from './scale';
16
16
 
17
17
  /**
@@ -22,7 +22,7 @@ import { minorAt, moneyScale } from './scale';
22
22
  */
23
23
  export const MAX_ALLOCATION_PARTS = 1_000_000;
24
24
 
25
- /** Split into `parts` equal shares. `allocate(money(100,'USD'), 3)` → 34, 33, 33. */
25
+ /** Split into `parts` equal shares. `allocate(fromMinor(100,'USD'), 3)` → 34, 33, 33. */
26
26
  export function allocate(amount: Money, parts: number): Money[] {
27
27
  if (!Number.isSafeInteger(parts) || parts <= 0) {
28
28
  throw allocationInvalid(`part count must be a positive integer, got ${String(parts)}`);
@@ -36,7 +36,7 @@ export function allocate(amount: Money, parts: number): Money[] {
36
36
  }
37
37
 
38
38
  /**
39
- * Split by weights. `allocateByRatios(money(1000,'USD'), [70, 20, 10])` → 700, 200, 100;
39
+ * Split by weights. `allocateByRatios(fromMinor(1000,'USD'), [70, 20, 10])` → 700, 200, 100;
40
40
  * `[1, 1, 1]` over 100 → 34, 33, 33. Weights need not sum to anything in particular.
41
41
  */
42
42
  export function allocateByRatios(amount: Money, ratios: readonly number[]): Money[] {
@@ -74,7 +74,7 @@ export function allocateByRatios(amount: Money, ratios: readonly number[]): Mone
74
74
  leftover -= 1n;
75
75
  }
76
76
 
77
- return floors.map((minor) => money(sign * Number(minor), amount.currency, amount.scale));
77
+ return floors.map((minor) => fromMinor(sign * Number(minor), amount.currency, amount.scale));
78
78
  }
79
79
 
80
80
  /**
package/src/arithmetic.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { allocationInvalid, currencyMismatch, currencyRequired } from './errors';
7
7
  import { factorFraction } from './factor';
8
- import { type Money, money } from './money';
8
+ import { fromMinor, type Money } from './money';
9
9
  import { DEFAULT_ROUNDING, type RoundingMode, roundRatio } from './rounding';
10
10
  import { commonScale, minorAt, toMinor } from './scale';
11
11
 
@@ -22,7 +22,7 @@ export function assertSameCurrency(left: Money, right: Money): string {
22
22
  export function add(left: Money, right: Money): Money {
23
23
  const currency = assertSameCurrency(left, right);
24
24
  const scale = commonScale(left, right);
25
- return money(
25
+ return fromMinor(
26
26
  toMinor(minorAt(left, scale) + minorAt(right, scale), scale, currency),
27
27
  currency,
28
28
  scale,
@@ -32,7 +32,7 @@ export function add(left: Money, right: Money): Money {
32
32
  export function subtract(left: Money, right: Money): Money {
33
33
  const currency = assertSameCurrency(left, right);
34
34
  const scale = commonScale(left, right);
35
- return money(
35
+ return fromMinor(
36
36
  toMinor(minorAt(left, scale) - minorAt(right, scale), scale, currency),
37
37
  currency,
38
38
  scale,
@@ -43,7 +43,7 @@ export function subtract(left: Money, right: Money): Money {
43
43
  * Every addend must share one currency; an empty list needs an explicit currency.
44
44
  *
45
45
  * A stated currency the first addend contradicts is `X_CURRENCY_MISMATCH`, not a silent win for
46
- * the list: `sum([money(1, 'EUR')], 'USD')` used to answer `{ minor: 1, currency: 'EUR' }`, so a
46
+ * the list: `sum([fromMinor(1, 'EUR')], 'USD')` used to answer `{ minor: 1, currency: 'EUR' }`, so a
47
47
  * caller who wrote down USD received EUR and nothing refused — the exact failure this file's
48
48
  * header exists to rule out, in the one entry point that treated its currency as a fallback rather
49
49
  * than as an assertion.
@@ -55,7 +55,7 @@ export function sum(amounts: readonly Money[], currency?: string): Money {
55
55
  }
56
56
  const base = first ?? currency;
57
57
  if (base === undefined) throw currencyRequired('sum([])');
58
- return amounts.reduce((total, amount) => add(total, amount), money(0, base));
58
+ return amounts.reduce((total, amount) => add(total, amount), fromMinor(0, base));
59
59
  }
60
60
 
61
61
  /**
@@ -73,7 +73,7 @@ export function multiply(
73
73
  ): Money {
74
74
  const ratio = factorFraction(factor);
75
75
  // Scale-preserving: a fee on a micro-priced amount stays a micro-priced amount.
76
- return money(
76
+ return fromMinor(
77
77
  roundRatio(BigInt(amount.minor) * ratio.numerator, ratio.denominator, mode),
78
78
  amount.currency,
79
79
  amount.scale,
@@ -94,7 +94,7 @@ export function divide(
94
94
  throw allocationInvalid('cannot divide money by zero — use allocate() to split a total');
95
95
  }
96
96
  const ratio = factorFraction(divisor);
97
- return money(
97
+ return fromMinor(
98
98
  roundRatio(BigInt(amount.minor) * ratio.denominator, ratio.numerator, mode),
99
99
  amount.currency,
100
100
  amount.scale,
@@ -102,11 +102,11 @@ export function divide(
102
102
  }
103
103
 
104
104
  export function negate(amount: Money): Money {
105
- return money(-amount.minor, amount.currency, amount.scale);
105
+ return fromMinor(-amount.minor, amount.currency, amount.scale);
106
106
  }
107
107
 
108
108
  export function absolute(amount: Money): Money {
109
- return money(Math.abs(amount.minor), amount.currency, amount.scale);
109
+ return fromMinor(Math.abs(amount.minor), amount.currency, amount.scale);
110
110
  }
111
111
 
112
112
  /**
package/src/convert.ts CHANGED
@@ -8,7 +8,7 @@ import { type Clock, systemClock } from '@ultimat3/core';
8
8
  import { assertCurrency, exponentOf } from './currency';
9
9
  import { rateMissing } from './errors';
10
10
  import { type Fraction, factorFraction } from './factor';
11
- import { type Money, money } from './money';
11
+ import { fromMinor, type Money } from './money';
12
12
  import { DEFAULT_ROUNDING, type RoundingMode, roundRatio } from './rounding';
13
13
  import { moneyScale } from './scale';
14
14
 
@@ -46,7 +46,7 @@ export interface ConvertOptions {
46
46
  }
47
47
 
48
48
  /**
49
- * `convert(money(1000,'USD'), 'EUR', { rate: 0.92, ... })`.
49
+ * `convert(fromMinor(1000,'USD'), 'EUR', { rate: 0.92, ... })`.
50
50
  * Scales across differing minor-unit exponents (USD 2 → JPY 0) instead of assuming both
51
51
  * sides have cents, and **preserves the amount's own `scale`**: a micro-priced amount stays
52
52
  * micro-priced in the target currency, exactly as `multiply` and `divide` keep theirs.
@@ -85,7 +85,7 @@ export function convert(
85
85
  const converted = roundRatio(numerator, denominator, options.rounding ?? DEFAULT_ROUNDING);
86
86
 
87
87
  return {
88
- amount: money(converted, target, resultScale),
88
+ amount: fromMinor(converted, target, resultScale),
89
89
  source: amount,
90
90
  rate: rate.rate,
91
91
  at: rate.at.toISOString(),
package/src/errors.ts CHANGED
@@ -84,7 +84,7 @@ export function moneyNotInteger(minor: number, currency: string): MoneyError {
84
84
  }
85
85
 
86
86
  /**
87
- * Three arrivals reach `money()` with an unusable `minor`, and each needs its own instruction.
87
+ * Three arrivals reach `fromMinor()` with an unusable `minor`, and each needs its own instruction.
88
88
  * One line answered all three — `fromDecimal('<minor>', '<ccy>')` — and it raised THIS code back
89
89
  * at the reader for every value past the currency's own digits, which is the anti-pattern
90
90
  * `currencyDeclarationInvalid` below already names; for `1e21` it read `fromDecimal('1e+21', …)`,
@@ -104,7 +104,7 @@ function notIntegerFix(minor: number, currency: string): string {
104
104
  // Safe to interpolate un-escaped, and only here: `PLAIN_DECIMAL` has just proved the spelling is
105
105
  // digits, one dot and one leading `-`, so it closes no literal `renderFixLiteral` would escape.
106
106
  const spelling = String(minor);
107
- const rounded = `money(Math.round(${spelling}), ${code})`;
107
+ const rounded = `fromMinor(Math.round(${spelling}), ${code})`;
108
108
  if (!PLAIN_DECIMAL.test(spelling)) {
109
109
  return `${rounded} — the value counts minor units, and that is the whole one nearest it`;
110
110
  }
@@ -182,7 +182,7 @@ export function scaleInvalid(scale: number): MoneyError {
182
182
  return new MoneyError({
183
183
  code: 'X_MONEY_SCALE_INVALID',
184
184
  cause: `a money scale must be a whole number of decimal places between 0 and ${MAX_MONEY_SCALE}, got ${String(scale)}`,
185
- fix: `use a scale in range — money(minor, currency, 6) for micros, or omit it for the currency's own minor unit`,
185
+ fix: `use a scale in range — fromMinor(minor, currency, 6) for micros, or omit it for the currency's own minor unit`,
186
186
  });
187
187
  }
188
188
 
package/src/format.ts CHANGED
@@ -30,7 +30,7 @@ export interface FormatMoneyOptions {
30
30
  }
31
31
 
32
32
  /**
33
- * `formatMoney(money(129900,'EUR'), 'de-DE')` → `1.299,00 €`.
33
+ * `formatMoney(fromMinor(129900,'EUR'), 'de-DE')` → `1.299,00 €`.
34
34
  *
35
35
  * Delegates to `formatMoneyParts` and joins: a UI styling the symbol off the parts and a label
36
36
  * rendering the string must not disagree about where the sign goes. Hand-prefixing `-` here put
package/src/index.ts CHANGED
@@ -80,9 +80,9 @@ export {
80
80
  type FromDecimalOptions,
81
81
  formatMoneyDebug,
82
82
  fromDecimal,
83
+ fromMinor,
83
84
  isMoney,
84
85
  type Money,
85
- money,
86
86
  toDecimalNumber,
87
87
  toDecimalString,
88
88
  zero,
@@ -95,4 +95,4 @@ export {
95
95
  roundToDigits,
96
96
  roundToInteger,
97
97
  } from './rounding';
98
- export { assertScale, commonScale, MAX_MONEY_SCALE, minorAt, moneyScale } from './scale';
98
+ export { assertScale, commonScale, minorAt, moneyScale } from './scale';
package/src/money.ts CHANGED
@@ -26,11 +26,11 @@ const DECIMAL = /^([+-])?(\d+)(?:\.(\d+))?$/;
26
26
  * The only constructor. Validates the currency and rejects fractional minor units.
27
27
  *
28
28
  * `scale` names how many decimal places `minor` counts when the currency's own are not enough:
29
- * `money(2, 'USD', 6)` is $0.000002. Omitted — and canonically omitted again when it says nothing
29
+ * `fromMinor(2, 'USD', 6)` is $0.000002. Omitted — and canonically omitted again when it says nothing
30
30
  * the currency does not already say — so a value at the natural scale serializes byte-for-byte as
31
31
  * it always has, and there is exactly one encoding of it.
32
32
  */
33
- export function money(minor: number, currency: string, scale?: number): Money {
33
+ export function fromMinor(minor: number, currency: string, scale?: number): Money {
34
34
  const code = assertCurrency(currency);
35
35
  if (!Number.isSafeInteger(minor)) throw moneyNotInteger(minor, code);
36
36
  // `-0` is one amount with two identities: `JSON.stringify` writes `0` while `Object.is` and any
@@ -45,7 +45,7 @@ export function money(minor: number, currency: string, scale?: number): Money {
45
45
  }
46
46
 
47
47
  export function zero(currency: string): Money {
48
- return money(0, currency);
48
+ return fromMinor(0, currency);
49
49
  }
50
50
 
51
51
  export interface FromDecimalOptions {
@@ -97,7 +97,7 @@ export function fromDecimal(
97
97
  );
98
98
  }
99
99
 
100
- return money(negative ? -minor : minor, code, options.scale);
100
+ return fromMinor(negative ? -minor : minor, code, options.scale);
101
101
  }
102
102
 
103
103
  /** `1299 EUR` → `'12.99'`; `1200 JPY` → `'1200'`; `2 USD @ scale 6` → `'0.000002'`. */
@@ -114,7 +114,7 @@ export function toDecimalString(amount: Money): string {
114
114
  /**
115
115
  * Major units as a float — lossy past 2^53 / 10^scale, so never for arithmetic and no longer for
116
116
  * formatting either: `format.ts` hands `Intl` the exact `toDecimalString`, since a float rendered
117
- * `money(9007199254740991, 'USD', 6)` as `…740992`. Kept as public API for a chart axis or a sort
117
+ * `fromMinor(9007199254740991, 'USD', 6)` as `…740992`. Kept as public API for a chart axis or a sort
118
118
  * key, where an approximation is the point.
119
119
  */
120
120
  export function toDecimalNumber(amount: Money): number {
package/src/rescale.ts CHANGED
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import { rescaleNotExact } from './errors';
8
- import { type Money, money } from './money';
8
+ import { fromMinor, type Money } from './money';
9
9
  import { type RoundingMode, roundRatio } from './rounding';
10
10
  import { assertScale, minorAt, moneyScale, toMinor } from './scale';
11
11
 
12
12
  /**
13
- * `rescale(money(80, 'USD'), 8)` → 80,000,000 hundred-millionths, the granularity a per-token
13
+ * `rescale(fromMinor(80, 'USD'), 8)` → 80,000,000 hundred-millionths, the granularity a per-token
14
14
  * price needs. `rescale(m, 2, 'half-up')` brings it back to cents, having named who pays for the
15
15
  * digits that go.
16
16
  */
@@ -18,7 +18,11 @@ export function rescale(amount: Money, scale: number, mode?: RoundingMode): Mone
18
18
  assertScale(scale);
19
19
  const from = moneyScale(amount);
20
20
  if (scale >= from) {
21
- return money(toMinor(minorAt(amount, scale), scale, amount.currency), amount.currency, scale);
21
+ return fromMinor(
22
+ toMinor(minorAt(amount, scale), scale, amount.currency),
23
+ amount.currency,
24
+ scale,
25
+ );
22
26
  }
23
27
 
24
28
  const divisor = 10n ** BigInt(from - scale);
@@ -27,5 +31,5 @@ export function rescale(amount: Money, scale: number, mode?: RoundingMode): Mone
27
31
  if (mode === undefined && numerator % divisor !== 0n) {
28
32
  throw rescaleNotExact(amount, from, scale);
29
33
  }
30
- return money(roundRatio(numerator, divisor, mode), amount.currency, scale);
34
+ return fromMinor(roundRatio(numerator, divisor, mode), amount.currency, scale);
31
35
  }
package/src/rounding.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * whichever one `Math.round` happens to implement is not an answer.
4
4
  */
5
5
 
6
- import { invariant } from '@ultimat3/core';
6
+ import { assertCoded } from '@ultimat3/core';
7
7
  import { MAX_MONEY_SCALE } from '@ultimat3/schema';
8
8
  import { digitsInvalid, notRoundable } from './errors';
9
9
  import { factorFraction } from './factor';
@@ -66,7 +66,7 @@ export function roundRatio(
66
66
  denominator: bigint,
67
67
  mode: RoundingMode = DEFAULT_ROUNDING,
68
68
  ): number {
69
- invariant(
69
+ assertCoded(
70
70
  denominator !== 0n,
71
71
  'X_INVARIANT',
72
72
  'cannot round a ratio whose denominator is zero',
@@ -98,7 +98,7 @@ export function roundRatio(
98
98
  else rounded = whole % 2n === 0n ? whole : whole + 1n;
99
99
  break;
100
100
  }
101
- // Past 2^53 the `Number` is already approximate, which `money()` refuses as X_MONEY_NOT_INTEGER.
101
+ // Past 2^53 the `Number` is already approximate, which `fromMinor()` refuses as X_MONEY_NOT_INTEGER.
102
102
  return Number(negative ? -rounded : rounded);
103
103
  }
104
104
 
package/src/scale.ts CHANGED
@@ -4,13 +4,11 @@
4
4
  * which is why nothing that predates this file has to change to keep meaning what it meant.
5
5
  */
6
6
 
7
- import { isMoneyScale, MAX_MONEY_SCALE } from '@ultimat3/schema';
7
+ import { isMoneyScale } from '@ultimat3/schema';
8
8
  import { exponentOf } from './currency';
9
9
  import { scaleInvalid, scaleNotWidening, scaleOverflow } from './errors';
10
10
  import type { Money } from './money';
11
11
 
12
- export { MAX_MONEY_SCALE };
13
-
14
12
  /**
15
13
  * The decimal exponent this value's `minor` counts in — its own, or the currency's.
16
14
  *
@@ -31,7 +29,7 @@ export function assertScale(scale: number): number {
31
29
  * `amount.minor` restated at `scale`, exactly, as a bigint.
32
30
  *
33
31
  * A bigint because widening is what a comparison does first, and a comparison must not throw:
34
- * `MAX_SAFE_INTEGER` cents restated in micros is past 2^53, which `money()` rightly refuses to
32
+ * `MAX_SAFE_INTEGER` cents restated in micros is past 2^53, which `fromMinor()` rightly refuses to
35
33
  * *store* and which says nothing about whether it is larger than the value beside it.
36
34
  *
37
35
  * Widening only. Narrowing drops digits, and which digits go is a decision with a mode attached —
@@ -52,7 +50,7 @@ export function commonScale(left: Money, right: Money): number {
52
50
  /**
53
51
  * A widened bigint back to a storable `minor`, or a scale error naming the finest scale that
54
52
  * would fit. The one place that conversion happens, because `Number(widened)` alone reached
55
- * `money()` as a plain out-of-range amount — reported as a fractional minor nobody wrote, with a
53
+ * `fromMinor()` as a plain out-of-range amount — reported as a fractional minor nobody wrote, with a
56
54
  * `fromDecimal` fix line that throws the same error again.
57
55
  */
58
56
  export function toMinor(widened: bigint, scale: number, currency: string): number {