@stll/money 0.1.0 → 0.2.2

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/dist/index.d.ts CHANGED
@@ -1,23 +1,10 @@
1
- //#region src/index.d.ts
1
+ //#region src/cents.d.ts
2
2
  /**
3
- * Branded monetary amounts and minor-unit billing arithmetic.
4
- *
5
- * Money in this codebase is stored and computed in minor units
6
- * ("cents" for USD/EUR, halere for CZK, etc.). The `CentsAmount`
7
- * brand prevents the canonical 100x bug where a major-unit value
8
- * (12.50 dollars) is silently mixed with minor-unit math (1250 cents).
3
+ * The minor-unit brand and its constructors.
9
4
  *
10
- * The brand lives here, in a shared package, so it threads end to end:
11
- * the same `CentsAmount` flows from a Drizzle column declared with
12
- * `.$type<CentsAmount>()`, across the API boundary (Eden infers the
13
- * brand from the handler's return type), into browser previews and back.
14
- * A plain `number` is not assignable to `CentsAmount`; mint one with
15
- * `cents()` after validating minor-unit input, or `unsafeCents()` at a
16
- * documented boundary.
17
- *
18
- * Currency-agnostic: the currency code lives alongside the amount in the
19
- * schema (e.g. invoices.currency); pairing them is the call site's
20
- * responsibility.
5
+ * Separate from `index.ts` because `format.ts` mints amounts too: the
6
+ * conversion helpers there return a `CentsAmount`, and a module the package
7
+ * entry re-exports cannot import back from that entry without a cycle.
21
8
  */
22
9
  declare const __cents: unique symbol;
23
10
  type CentsAmount = number & {
@@ -26,30 +13,108 @@ type CentsAmount = number & {
26
13
  /**
27
14
  * Construct a CentsAmount from a value already known to be in minor
28
15
  * units. Use at boundaries where the input is validated as an integer
29
- * minor-unit value (e.g. after Elysia `t.Integer({ minimum: 0 })` or
30
- * after parsing user input that has been multiplied by 100).
16
+ * minor-unit value (e.g. after Elysia `tMinorUnitAmount(...)` or after
17
+ * scaling a typed major-unit amount by the currency's exponent).
31
18
  *
32
- * Throws on non-integer input — money math at the minor-unit level must
33
- * be exact.
19
+ * SAFE integer, not merely integer: `Number.isInteger` is true for 2^53 and
20
+ * everything above it, where the spacing between representable doubles is
21
+ * larger than one minor unit, so `x + 1 === x` and a total silently stops
22
+ * moving. An amount that far out is a caller defect rather than a runtime
23
+ * condition, like a fractional one, so it panics the same way.
34
24
  */
35
- declare const cents: (value: number) => CentsAmount;
25
+ export declare const cents: (value: number) => CentsAmount;
36
26
  /**
37
27
  * Escape hatch for code paths that genuinely need to attach the brand
38
28
  * without a runtime check (test fixtures, generated code). Prefer
39
29
  * `cents()` everywhere else; reach for this only with a `// SAFETY:`
40
30
  * comment naming why the value is already a valid minor-unit integer.
41
31
  */
42
- declare const unsafeCents: (value: number) => CentsAmount;
43
- type ProrateHourlyCentsInput = {
32
+ export declare const unsafeCents: (value: number) => CentsAmount;
33
+ //#endregion
34
+ //#region src/format.d.ts
35
+ /**
36
+ * How many minor units make a major one, for this currency: 100 for CZK, 1 for
37
+ * JPY, 1000 for KWD. It is a property of the currency and not of the reader, so
38
+ * the lookup is deliberately locale-independent.
39
+ *
40
+ * A malformed code makes the `Intl.NumberFormat` constructor throw, before any
41
+ * `?? 2` on its result could help, so the fallback has to wrap the call.
42
+ */
43
+ export declare const currencyMinorUnitDigits: (currency: string) => number;
44
+ type ToMinorUnitsParams = {
45
+ /**
46
+ * The amount in major units: the decimal text a form holds, or a number.
47
+ * Text is preferred where the caller has it, because it is what the person
48
+ * typed rather than the nearest double to it.
49
+ */
50
+ amount: number | string;
51
+ currency: string;
52
+ };
53
+ /**
54
+ * A typed major-unit amount as the minor units the currency actually counts:
55
+ * 12.5 USD is 1250, 1500 JPY is 1500, 12.5 KWD is 12500.
56
+ *
57
+ * The scaling is decimal, not a float multiply. `1.005 * 100` is
58
+ * 100.49999999999999 in binary floating point, so `Math.round` of it is 100
59
+ * and a $1.005 line item silently loses a cent; the same shortfall appears at
60
+ * a different decimal for every currency. Splitting the text on the point and
61
+ * moving the digits instead keeps the amount the person typed: the kept
62
+ * fraction digits are appended to the whole part, and the first digit dropped
63
+ * decides a half-up carry on the magnitude.
64
+ *
65
+ * Rounding is still the point -- a typed amount carries more places than the
66
+ * currency has -- and it happens on digits, exactly. Text this cannot parse,
67
+ * and a result outside the safe integer range, are caller defects: gate the
68
+ * text with `isDecimalAmount` first.
69
+ */
70
+ export declare const toMinorUnits: (params: ToMinorUnitsParams) => CentsAmount;
71
+ /**
72
+ * The same conversion for text nobody has vouched for yet: null when the text
73
+ * is not a decimal amount, and null when the scaled value would leave the safe
74
+ * integer range, where a total silently stops adding up.
75
+ *
76
+ * A form holds whatever was typed and has to be able to decline it. Everything
77
+ * past that gate calls `toMinorUnits`, which panics, because by then the
78
+ * decision has been made.
79
+ */
80
+ export declare const tryToMinorUnits: ({ amount, currency }: ToMinorUnitsParams) => CentsAmount | null;
81
+ type ToMajorUnitsParams = {
82
+ amountCents: number;
83
+ currency: string;
84
+ };
85
+ /** The inverse: a stored amount as the major-unit number a person reads. */
86
+ export declare const toMajorUnits: ({ amountCents, currency }: ToMajorUnitsParams) => number;
87
+ type FormatMoneyCentsParams = {
88
+ amountCents: number;
89
+ currency: string;
90
+ locale: string;
91
+ /**
92
+ * Digits to show, minimum and maximum alike. Defaults to the currency's own
93
+ * exponent; pass 0 for a rounded summary that has no room for decimals.
94
+ */
95
+ fractionDigits?: number;
96
+ };
97
+ /**
98
+ * Money is stored in minor units, and how many of them make a major one is a
99
+ * property of the currency. Ask the currency rather than assuming a hundred.
100
+ *
101
+ * A code `Intl` rejects falls back to the amount beside the raw code: a column
102
+ * showing "15.00 A1C" is wrong-looking data, which is the truth, where a thrown
103
+ * RangeError would take the whole board down with it.
104
+ */
105
+ export declare const formatMoneyCents: ({ amountCents, currency, locale, fractionDigits }: FormatMoneyCentsParams) => string;
106
+ //#endregion
107
+ //#region src/index.d.ts
108
+ export type ProrateHourlyCentsInput = {
44
109
  billedMinutes: number;
45
110
  hourlyRateCents: CentsAmount;
46
111
  };
47
- declare const prorateHourlyCents: ({ billedMinutes, hourlyRateCents }: ProrateHourlyCentsInput) => CentsAmount;
48
- type ApplyMarkupCentsInput = {
112
+ export declare const prorateHourlyCents: ({ billedMinutes, hourlyRateCents }: ProrateHourlyCentsInput) => CentsAmount;
113
+ export type ApplyMarkupCentsInput = {
49
114
  amountCents: CentsAmount;
50
115
  markupPercent: number;
51
116
  };
52
- declare const applyMarkupCents: ({ amountCents, markupPercent }: ApplyMarkupCentsInput) => CentsAmount;
117
+ export declare const applyMarkupCents: ({ amountCents, markupPercent }: ApplyMarkupCentsInput) => CentsAmount;
53
118
  declare const __currency: unique symbol;
54
119
  /**
55
120
  * A `CentsAmount` additionally branded with its ISO 4217-ish currency code
@@ -63,7 +128,7 @@ declare const __currency: unique symbol;
63
128
  * currency code is a plain string carried alongside it, so there is no
64
129
  * boundary that needs to skip it.
65
130
  */
66
- type CurrencyCents<C extends string = string> = CentsAmount & {
131
+ export type CurrencyCents<C extends string = string> = CentsAmount & {
67
132
  readonly [__currency]: C;
68
133
  };
69
134
  /**
@@ -72,7 +137,7 @@ type CurrencyCents<C extends string = string> = CentsAmount & {
72
137
  * from the literal `currency` argument (e.g. `currencyCents("USD", 100)`
73
138
  * infers `CurrencyCents<"USD">`).
74
139
  */
75
- declare const currencyCents: <C extends string>(currency: C, amount: number) => CurrencyCents<C>;
140
+ export declare const currencyCents: <C extends string>(currency: C, amount: number) => CurrencyCents<C>;
76
141
  type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
77
142
  type IsSingletonCurrency<C extends string> = string extends C ? never : [C] extends [UnionToIntersection<C>] ? C : never;
78
143
  /**
@@ -96,7 +161,7 @@ type IsSingletonCurrency<C extends string> = string extends C ? never : [C] exte
96
161
  * instead, which buckets by currency at runtime and is the runtime-correct
97
162
  * tool for that case.
98
163
  */
99
- declare const addCents: <A extends string, B extends A = A>(a: IsSingletonCurrency<A> extends never ? never : CurrencyCents<A>, b: IsSingletonCurrency<B> extends never ? never : CurrencyCents<B>) => CurrencyCents<A>;
164
+ export declare const addCents: <A extends string, B extends A = A>(a: IsSingletonCurrency<A> extends never ? never : CurrencyCents<A>, b: IsSingletonCurrency<B> extends never ? never : CurrencyCents<B>) => CurrencyCents<A>;
100
165
  /**
101
166
  * Per-currency accumulator for aggregating money across rows that may carry
102
167
  * different currencies (e.g. time entries across matters, expenses across
@@ -117,11 +182,11 @@ declare const addCents: <A extends string, B extends A = A>(a: IsSingletonCurren
117
182
  * the same "never sum across currencies" invariant at runtime via the
118
183
  * per-currency `Map`.
119
184
  */
120
- type MoneyTotalsEntry = {
185
+ export type MoneyTotalsEntry = {
121
186
  currency: string;
122
187
  amountCents: CentsAmount;
123
188
  };
124
- declare class MoneyTotals {
189
+ export declare class MoneyTotals {
125
190
  #private;
126
191
  /** Add `amountCents` to the running total for `currency`. */
127
192
  add(currency: string, amountCents: CentsAmount): void;
@@ -134,4 +199,4 @@ declare class MoneyTotals {
134
199
  entries(): MoneyTotalsEntry[];
135
200
  }
136
201
  //#endregion
137
- export { ApplyMarkupCentsInput, CentsAmount, CurrencyCents, MoneyTotals, MoneyTotalsEntry, ProrateHourlyCentsInput, addCents, applyMarkupCents, cents, currencyCents, prorateHourlyCents, unsafeCents };
202
+ export type { CentsAmount, FormatMoneyCentsParams, ToMajorUnitsParams, ToMinorUnitsParams };
package/dist/index.js CHANGED
@@ -1,15 +1,28 @@
1
- //#region src/index.ts
1
+ import { Result, panic } from "better-result";
2
+ //#region src/cents.ts
3
+ /**
4
+ * The minor-unit brand and its constructors.
5
+ *
6
+ * Separate from `index.ts` because `format.ts` mints amounts too: the
7
+ * conversion helpers there return a `CentsAmount`, and a module the package
8
+ * entry re-exports cannot import back from that entry without a cycle.
9
+ */
10
+ /** The brand is nominal: every safe integer is a valid minor-unit amount. */
11
+ const isMinorUnitAmount = (value) => Number.isSafeInteger(value);
2
12
  /**
3
13
  * Construct a CentsAmount from a value already known to be in minor
4
14
  * units. Use at boundaries where the input is validated as an integer
5
- * minor-unit value (e.g. after Elysia `t.Integer({ minimum: 0 })` or
6
- * after parsing user input that has been multiplied by 100).
15
+ * minor-unit value (e.g. after Elysia `tMinorUnitAmount(...)` or after
16
+ * scaling a typed major-unit amount by the currency's exponent).
7
17
  *
8
- * Throws on non-integer input — money math at the minor-unit level must
9
- * be exact.
18
+ * SAFE integer, not merely integer: `Number.isInteger` is true for 2^53 and
19
+ * everything above it, where the spacing between representable doubles is
20
+ * larger than one minor unit, so `x + 1 === x` and a total silently stops
21
+ * moving. An amount that far out is a caller defect rather than a runtime
22
+ * condition, like a fractional one, so it panics the same way.
10
23
  */
11
24
  const cents = (value) => {
12
- if (!Number.isInteger(value)) throw new TypeError(`cents(${value}): money values must be integer minor units`);
25
+ if (!isMinorUnitAmount(value)) return panic(`cents(${value}): money values must be safe integer minor units`);
13
26
  return value;
14
27
  };
15
28
  /**
@@ -19,6 +32,180 @@ const cents = (value) => {
19
32
  * comment naming why the value is already a valid minor-unit integer.
20
33
  */
21
34
  const unsafeCents = (value) => value;
35
+ //#endregion
36
+ //#region src/format.ts
37
+ /**
38
+ * Moving between a stored minor-unit amount and the major-unit number a
39
+ * person types, and rendering either as text.
40
+ *
41
+ * The brand keeps the arithmetic honest; this keeps the scale honest. Both
42
+ * live here because "how many minor units make a major one" is a property of
43
+ * the currency, and every surface that touches money has to answer it the
44
+ * same way — a workspace column header, a billing form, an invoice line, an
45
+ * export row.
46
+ *
47
+ * The locale is always a parameter. A package cannot read the reader's
48
+ * formatting locale, and one that guessed would render a number differently
49
+ * from the app around it.
50
+ */
51
+ /**
52
+ * Two, the ISO 4217 default, used when a stored code is one `Intl` will not
53
+ * accept. Validation at the API boundary keeps those out, but a row written
54
+ * before that constraint existed must still render rather than throw.
55
+ */
56
+ const DEFAULT_MINOR_UNIT_DIGITS = 2;
57
+ /**
58
+ * How many minor units make a major one, for this currency: 100 for CZK, 1 for
59
+ * JPY, 1000 for KWD. It is a property of the currency and not of the reader, so
60
+ * the lookup is deliberately locale-independent.
61
+ *
62
+ * A malformed code makes the `Intl.NumberFormat` constructor throw, before any
63
+ * `?? 2` on its result could help, so the fallback has to wrap the call.
64
+ */
65
+ const currencyMinorUnitDigits = (currency) => {
66
+ const resolved = Result.try(() => new Intl.NumberFormat("en", {
67
+ style: "currency",
68
+ currency
69
+ }).resolvedOptions().maximumFractionDigits);
70
+ if (resolved.isErr()) return DEFAULT_MINOR_UNIT_DIGITS;
71
+ return resolved.value ?? DEFAULT_MINOR_UNIT_DIGITS;
72
+ };
73
+ /** A decimal amount in major units: an optional sign, digits, an optional point. */
74
+ const DECIMAL_AMOUNT = /^(?<sign>[+-]?)(?<whole>\d*)(?:\.(?<fraction>\d*))?$/u;
75
+ /** Scientific notation, which `String(1e21)` and `String(1e-7)` both produce. */
76
+ const EXPONENT_AMOUNT = /^(?<sign>[+-]?)(?<whole>\d+)(?:\.(?<fraction>\d+))?[eE](?<exponent>[+-]?\d+)$/u;
77
+ /**
78
+ * The widest exponent a finite double prints with (`5e-324`, `1.8e308`).
79
+ * Text past it names no amount any currency stores, and expanding it would
80
+ * mean materialising that many zeros.
81
+ */
82
+ const MAX_EXPONENT_MAGNITUDE = 324;
83
+ const expandExponent = (text) => {
84
+ const groups = EXPONENT_AMOUNT.exec(text)?.groups;
85
+ if (!groups) return null;
86
+ const exponent = Number(groups["exponent"]);
87
+ if (Math.abs(exponent) > MAX_EXPONENT_MAGNITUDE) return null;
88
+ const sign = groups["sign"] ?? "";
89
+ const digits = `${groups["whole"] ?? ""}${groups["fraction"] ?? ""}`;
90
+ const point = (groups["whole"] ?? "").length + exponent;
91
+ if (point <= 0) return `${sign}0.${"0".repeat(-point)}${digits}`;
92
+ if (point >= digits.length) return `${sign}${digits}${"0".repeat(point - digits.length)}`;
93
+ return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
94
+ };
95
+ /**
96
+ * The amount as decimal text, or null when it is not a decimal amount.
97
+ *
98
+ * A number is rendered by `String`, which produces the SHORTEST text that
99
+ * round-trips back to the same double. That is what makes the number path
100
+ * exact: the double nearest 1.005 prints as "1.005", which is the amount the
101
+ * person typed, where multiplying that double by 100 yields 100.49999999999999.
102
+ */
103
+ const decimalText = (amount) => {
104
+ if (typeof amount === "number") {
105
+ if (!Number.isFinite(amount)) return null;
106
+ const rendered = String(amount);
107
+ return rendered.includes("e") ? expandExponent(rendered) : rendered;
108
+ }
109
+ const trimmed = amount.trim();
110
+ return /[eE]/u.test(trimmed) ? expandExponent(trimmed) : trimmed;
111
+ };
112
+ /** The digits of a decimal amount, or null when the text is not one. */
113
+ const amountDigits = (amount) => {
114
+ const text = decimalText(amount);
115
+ const groups = text === null ? void 0 : DECIMAL_AMOUNT.exec(text)?.groups;
116
+ if (groups === void 0) return null;
117
+ const whole = groups["whole"] ?? "";
118
+ const fraction = groups["fraction"] ?? "";
119
+ if (`${whole}${fraction}`.length === 0) return null;
120
+ return {
121
+ sign: groups["sign"] ?? "",
122
+ whole,
123
+ fraction
124
+ };
125
+ };
126
+ /**
127
+ * A typed major-unit amount as the minor units the currency actually counts:
128
+ * 12.5 USD is 1250, 1500 JPY is 1500, 12.5 KWD is 12500.
129
+ *
130
+ * The scaling is decimal, not a float multiply. `1.005 * 100` is
131
+ * 100.49999999999999 in binary floating point, so `Math.round` of it is 100
132
+ * and a $1.005 line item silently loses a cent; the same shortfall appears at
133
+ * a different decimal for every currency. Splitting the text on the point and
134
+ * moving the digits instead keeps the amount the person typed: the kept
135
+ * fraction digits are appended to the whole part, and the first digit dropped
136
+ * decides a half-up carry on the magnitude.
137
+ *
138
+ * Rounding is still the point -- a typed amount carries more places than the
139
+ * currency has -- and it happens on digits, exactly. Text this cannot parse,
140
+ * and a result outside the safe integer range, are caller defects: gate the
141
+ * text with `isDecimalAmount` first.
142
+ */
143
+ const toMinorUnits = (params) => tryToMinorUnits(params) ?? panic(`toMinorUnits(${JSON.stringify(params.amount)}): not an amount ${params.currency} can store`);
144
+ /**
145
+ * The same conversion for text nobody has vouched for yet: null when the text
146
+ * is not a decimal amount, and null when the scaled value would leave the safe
147
+ * integer range, where a total silently stops adding up.
148
+ *
149
+ * A form holds whatever was typed and has to be able to decline it. Everything
150
+ * past that gate calls `toMinorUnits`, which panics, because by then the
151
+ * decision has been made.
152
+ */
153
+ const tryToMinorUnits = ({ amount, currency }) => {
154
+ const parsed = amountDigits(amount);
155
+ if (parsed === null) return null;
156
+ const digits = currencyMinorUnitDigits(currency);
157
+ const kept = parsed.fraction.slice(0, digits).padEnd(digits, "0");
158
+ const dropped = parsed.fraction.charAt(digits);
159
+ const magnitude = BigInt(`${parsed.whole || "0"}${kept}`) + (dropped >= "5" ? 1n : 0n);
160
+ const value = Number(parsed.sign === "-" ? -magnitude : magnitude);
161
+ return Number.isSafeInteger(value) ? cents(value) : null;
162
+ };
163
+ /** The inverse: a stored amount as the major-unit number a person reads. */
164
+ const toMajorUnits = ({ amountCents, currency }) => amountCents / 10 ** currencyMinorUnitDigits(currency);
165
+ /**
166
+ * Money is stored in minor units, and how many of them make a major one is a
167
+ * property of the currency. Ask the currency rather than assuming a hundred.
168
+ *
169
+ * A code `Intl` rejects falls back to the amount beside the raw code: a column
170
+ * showing "15.00 A1C" is wrong-looking data, which is the truth, where a thrown
171
+ * RangeError would take the whole board down with it.
172
+ */
173
+ const formatMoneyCents = ({ amountCents, currency, locale, fractionDigits }) => {
174
+ const major = toMajorUnits({
175
+ amountCents,
176
+ currency
177
+ });
178
+ const digits = fractionDigits ?? currencyMinorUnitDigits(currency);
179
+ const formatted = Result.try(() => new Intl.NumberFormat(locale, {
180
+ style: "currency",
181
+ currency,
182
+ minimumFractionDigits: digits,
183
+ maximumFractionDigits: digits
184
+ }).format(major));
185
+ return formatted.isErr() ? `${major.toFixed(digits)} ${currency}` : formatted.value;
186
+ };
187
+ //#endregion
188
+ //#region src/index.ts
189
+ /**
190
+ * Branded monetary amounts and minor-unit billing arithmetic.
191
+ *
192
+ * Money in this codebase is stored and computed in minor units
193
+ * ("cents" for USD/EUR, halere for CZK, etc.). The `CentsAmount`
194
+ * brand prevents the canonical 100x bug where a major-unit value
195
+ * (12.50 dollars) is silently mixed with minor-unit math (1250 cents).
196
+ *
197
+ * The brand lives in this package, so it threads end to end:
198
+ * the same `CentsAmount` flows from a Drizzle column declared with
199
+ * `.$type<CentsAmount>()`, across the API boundary (Eden infers the
200
+ * brand from the handler's return type), into browser previews and back.
201
+ * A plain `number` is not assignable to `CentsAmount`; mint one with
202
+ * `cents()` after validating minor-unit input, or `unsafeCents()` at a
203
+ * documented boundary.
204
+ *
205
+ * Currency-agnostic: the currency code lives alongside the amount in the
206
+ * schema (e.g. invoices.currency); pairing them is the call site's
207
+ * responsibility.
208
+ */
22
209
  const prorateHourlyCents = ({ billedMinutes, hourlyRateCents }) => {
23
210
  assertNonNegativeInteger("billedMinutes", billedMinutes);
24
211
  assertNonNegativeInteger("hourlyRateCents", hourlyRateCents);
@@ -30,8 +217,9 @@ const applyMarkupCents = ({ amountCents, markupPercent }) => {
30
217
  return cents(Math.floor((amountCents * (100 + markupPercent) + 50) / 100));
31
218
  };
32
219
  function assertNonNegativeInteger(name, value) {
33
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a finite non-negative integer`);
220
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) panic(`${name} must be a finite non-negative integer`);
34
221
  }
222
+ const isCurrencyCents = (amount, currency) => currency !== "" && isMinorUnitAmount(amount);
35
223
  /**
36
224
  * Construct a `CurrencyCents<C>` from a currency code and a minor-unit
37
225
  * amount. The only producer of `CurrencyCents`; downstream code narrows `C`
@@ -39,8 +227,9 @@ function assertNonNegativeInteger(name, value) {
39
227
  * infers `CurrencyCents<"USD">`).
40
228
  */
41
229
  const currencyCents = (currency, amount) => {
42
- if (!currency) throw new TypeError("currencyCents(): currency must be a non-empty code");
43
- return cents(amount);
230
+ if (!currency) return panic("currencyCents(): currency must be a non-empty code");
231
+ const minorUnits = cents(amount);
232
+ return isCurrencyCents(minorUnits, currency) ? minorUnits : panic("currencyCents(): currency must be a non-empty code");
44
233
  };
45
234
  /**
46
235
  * Add two `CurrencyCents` amounts of the SAME currency. The second
@@ -68,7 +257,7 @@ var MoneyTotals = class {
68
257
  #totals = /* @__PURE__ */ new Map();
69
258
  /** Add `amountCents` to the running total for `currency`. */
70
259
  add(currency, amountCents) {
71
- if (!currency) throw new TypeError("MoneyTotals.add(): currency must be a non-empty code");
260
+ if (!currency) panic("MoneyTotals.add(): currency must be a non-empty code");
72
261
  const running = this.#totals.get(currency) ?? cents(0);
73
262
  this.#totals.set(currency, cents(running + amountCents));
74
263
  }
@@ -79,11 +268,11 @@ var MoneyTotals = class {
79
268
  * `localeCompare`, so ordering does not vary with the runtime's locale.
80
269
  */
81
270
  entries() {
82
- return [...this.#totals.keys()].sort().map((currency) => ({
271
+ return [...this.#totals.keys()].toSorted().map((currency) => ({
83
272
  currency,
84
273
  amountCents: this.#totals.get(currency) ?? cents(0)
85
274
  }));
86
275
  }
87
276
  };
88
277
  //#endregion
89
- export { MoneyTotals, addCents, applyMarkupCents, cents, currencyCents, prorateHourlyCents, unsafeCents };
278
+ export { MoneyTotals, addCents, applyMarkupCents, cents, currencyCents, currencyMinorUnitDigits, formatMoneyCents, prorateHourlyCents, toMajorUnits, toMinorUnits, tryToMinorUnits, unsafeCents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/money",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "Branded minor-unit monetary amounts and currency-safe billing arithmetic.",
5
5
  "keywords": [
6
6
  "billing",
@@ -40,15 +40,18 @@
40
40
  "pack:dry-run": "bun pm pack --dry-run",
41
41
  "test": "bun test src",
42
42
  "typecheck": "bun ../../packages/scripts/src/tsc-native.ts --noEmit",
43
- "lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware packages/money",
43
+ "lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --type-aware packages/money",
44
44
  "lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/money",
45
- "format": "oxfmt .",
45
+ "format": "bun ../../scripts/run-oxfmt.ts .",
46
46
  "prepack": "bun run build"
47
47
  },
48
+ "dependencies": {
49
+ "better-result": "3.0.1"
50
+ },
48
51
  "devDependencies": {
49
52
  "@stll/typescript-config": "0.0.0",
50
- "@types/bun": "1.3.14",
51
- "tsdown": "0.22.14"
53
+ "bun-types": "1.4.2",
54
+ "tsdown": "0.23.0"
52
55
  },
53
56
  "main": "./dist/index.js",
54
57
  "types": "./dist/index.d.ts"