@stll/money 0.2.0 → 0.2.3

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
@@ -22,14 +22,14 @@ type CentsAmount = number & {
22
22
  * moving. An amount that far out is a caller defect rather than a runtime
23
23
  * condition, like a fractional one, so it panics the same way.
24
24
  */
25
- declare const cents: (value: number) => CentsAmount;
25
+ export declare const cents: (value: number) => CentsAmount;
26
26
  /**
27
27
  * Escape hatch for code paths that genuinely need to attach the brand
28
28
  * without a runtime check (test fixtures, generated code). Prefer
29
29
  * `cents()` everywhere else; reach for this only with a `// SAFETY:`
30
30
  * comment naming why the value is already a valid minor-unit integer.
31
31
  */
32
- declare const unsafeCents: (value: number) => CentsAmount;
32
+ export declare const unsafeCents: (value: number) => CentsAmount;
33
33
  //#endregion
34
34
  //#region src/format.d.ts
35
35
  /**
@@ -40,7 +40,7 @@ declare const unsafeCents: (value: number) => CentsAmount;
40
40
  * A malformed code makes the `Intl.NumberFormat` constructor throw, before any
41
41
  * `?? 2` on its result could help, so the fallback has to wrap the call.
42
42
  */
43
- declare const currencyMinorUnitDigits: (currency: string) => number;
43
+ export declare const currencyMinorUnitDigits: (currency: string) => number;
44
44
  type ToMinorUnitsParams = {
45
45
  /**
46
46
  * The amount in major units: the decimal text a form holds, or a number.
@@ -67,7 +67,7 @@ type ToMinorUnitsParams = {
67
67
  * and a result outside the safe integer range, are caller defects: gate the
68
68
  * text with `isDecimalAmount` first.
69
69
  */
70
- declare const toMinorUnits: (params: ToMinorUnitsParams) => CentsAmount;
70
+ export declare const toMinorUnits: (params: ToMinorUnitsParams) => CentsAmount;
71
71
  /**
72
72
  * The same conversion for text nobody has vouched for yet: null when the text
73
73
  * is not a decimal amount, and null when the scaled value would leave the safe
@@ -77,13 +77,13 @@ declare const toMinorUnits: (params: ToMinorUnitsParams) => CentsAmount;
77
77
  * past that gate calls `toMinorUnits`, which panics, because by then the
78
78
  * decision has been made.
79
79
  */
80
- declare const tryToMinorUnits: ({ amount, currency }: ToMinorUnitsParams) => CentsAmount | null;
80
+ export declare const tryToMinorUnits: ({ amount, currency }: ToMinorUnitsParams) => CentsAmount | null;
81
81
  type ToMajorUnitsParams = {
82
82
  amountCents: number;
83
83
  currency: string;
84
84
  };
85
85
  /** The inverse: a stored amount as the major-unit number a person reads. */
86
- declare const toMajorUnits: ({ amountCents, currency }: ToMajorUnitsParams) => number;
86
+ export declare const toMajorUnits: ({ amountCents, currency }: ToMajorUnitsParams) => number;
87
87
  type FormatMoneyCentsParams = {
88
88
  amountCents: number;
89
89
  currency: string;
@@ -102,19 +102,19 @@ type FormatMoneyCentsParams = {
102
102
  * showing "15.00 A1C" is wrong-looking data, which is the truth, where a thrown
103
103
  * RangeError would take the whole board down with it.
104
104
  */
105
- declare const formatMoneyCents: ({ amountCents, currency, locale, fractionDigits }: FormatMoneyCentsParams) => string;
105
+ export declare const formatMoneyCents: ({ amountCents, currency, locale, fractionDigits }: FormatMoneyCentsParams) => string;
106
106
  //#endregion
107
107
  //#region src/index.d.ts
108
- type ProrateHourlyCentsInput = {
108
+ export type ProrateHourlyCentsInput = {
109
109
  billedMinutes: number;
110
110
  hourlyRateCents: CentsAmount;
111
111
  };
112
- declare const prorateHourlyCents: ({ billedMinutes, hourlyRateCents }: ProrateHourlyCentsInput) => CentsAmount;
113
- type ApplyMarkupCentsInput = {
112
+ export declare const prorateHourlyCents: ({ billedMinutes, hourlyRateCents }: ProrateHourlyCentsInput) => CentsAmount;
113
+ export type ApplyMarkupCentsInput = {
114
114
  amountCents: CentsAmount;
115
115
  markupPercent: number;
116
116
  };
117
- declare const applyMarkupCents: ({ amountCents, markupPercent }: ApplyMarkupCentsInput) => CentsAmount;
117
+ export declare const applyMarkupCents: ({ amountCents, markupPercent }: ApplyMarkupCentsInput) => CentsAmount;
118
118
  declare const __currency: unique symbol;
119
119
  /**
120
120
  * A `CentsAmount` additionally branded with its ISO 4217-ish currency code
@@ -128,7 +128,7 @@ declare const __currency: unique symbol;
128
128
  * currency code is a plain string carried alongside it, so there is no
129
129
  * boundary that needs to skip it.
130
130
  */
131
- type CurrencyCents<C extends string = string> = CentsAmount & {
131
+ export type CurrencyCents<C extends string = string> = CentsAmount & {
132
132
  readonly [__currency]: C;
133
133
  };
134
134
  /**
@@ -137,7 +137,7 @@ type CurrencyCents<C extends string = string> = CentsAmount & {
137
137
  * from the literal `currency` argument (e.g. `currencyCents("USD", 100)`
138
138
  * infers `CurrencyCents<"USD">`).
139
139
  */
140
- 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>;
141
141
  type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
142
142
  type IsSingletonCurrency<C extends string> = string extends C ? never : [C] extends [UnionToIntersection<C>] ? C : never;
143
143
  /**
@@ -161,7 +161,7 @@ type IsSingletonCurrency<C extends string> = string extends C ? never : [C] exte
161
161
  * instead, which buckets by currency at runtime and is the runtime-correct
162
162
  * tool for that case.
163
163
  */
164
- 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>;
165
165
  /**
166
166
  * Per-currency accumulator for aggregating money across rows that may carry
167
167
  * different currencies (e.g. time entries across matters, expenses across
@@ -182,11 +182,11 @@ declare const addCents: <A extends string, B extends A = A>(a: IsSingletonCurren
182
182
  * the same "never sum across currencies" invariant at runtime via the
183
183
  * per-currency `Map`.
184
184
  */
185
- type MoneyTotalsEntry = {
185
+ export type MoneyTotalsEntry = {
186
186
  currency: string;
187
187
  amountCents: CentsAmount;
188
188
  };
189
- declare class MoneyTotals {
189
+ export declare class MoneyTotals {
190
190
  #private;
191
191
  /** Add `amountCents` to the running total for `currency`. */
192
192
  add(currency: string, amountCents: CentsAmount): void;
@@ -199,4 +199,4 @@ declare class MoneyTotals {
199
199
  entries(): MoneyTotalsEntry[];
200
200
  }
201
201
  //#endregion
202
- export { ApplyMarkupCentsInput, type CentsAmount, CurrencyCents, type FormatMoneyCentsParams, MoneyTotals, MoneyTotalsEntry, ProrateHourlyCentsInput, type ToMajorUnitsParams, type ToMinorUnitsParams, addCents, applyMarkupCents, cents, currencyCents, currencyMinorUnitDigits, formatMoneyCents, prorateHourlyCents, toMajorUnits, toMinorUnits, tryToMinorUnits, unsafeCents };
202
+ export type { CentsAmount, FormatMoneyCentsParams, ToMajorUnitsParams, ToMinorUnitsParams };
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import { Result, panic } from "better-result";
7
7
  * conversion helpers there return a `CentsAmount`, and a module the package
8
8
  * entry re-exports cannot import back from that entry without a cycle.
9
9
  */
10
+ /** The brand is nominal: every safe integer is a valid minor-unit amount. */
11
+ const isMinorUnitAmount = (value) => Number.isSafeInteger(value);
10
12
  /**
11
13
  * Construct a CentsAmount from a value already known to be in minor
12
14
  * units. Use at boundaries where the input is validated as an integer
@@ -20,7 +22,7 @@ import { Result, panic } from "better-result";
20
22
  * condition, like a fractional one, so it panics the same way.
21
23
  */
22
24
  const cents = (value) => {
23
- if (!Number.isSafeInteger(value)) return panic(`cents(${value}): money values must be safe integer minor units`);
25
+ if (!isMinorUnitAmount(value)) return panic(`cents(${value}): money values must be safe integer minor units`);
24
26
  return value;
25
27
  };
26
28
  /**
@@ -207,16 +209,19 @@ const formatMoneyCents = ({ amountCents, currency, locale, fractionDigits }) =>
207
209
  const prorateHourlyCents = ({ billedMinutes, hourlyRateCents }) => {
208
210
  assertNonNegativeInteger("billedMinutes", billedMinutes);
209
211
  assertNonNegativeInteger("hourlyRateCents", hourlyRateCents);
210
- return cents(Math.floor((billedMinutes * hourlyRateCents + 30) / 60));
212
+ const product = BigInt(billedMinutes) * BigInt(hourlyRateCents);
213
+ return cents(Number((product + 30n) / 60n));
211
214
  };
212
215
  const applyMarkupCents = ({ amountCents, markupPercent }) => {
213
216
  assertNonNegativeInteger("amountCents", amountCents);
214
217
  assertNonNegativeInteger("markupPercent", markupPercent);
215
- return cents(Math.floor((amountCents * (100 + markupPercent) + 50) / 100));
218
+ const product = BigInt(amountCents) * (100n + BigInt(markupPercent));
219
+ return cents(Number((product + 50n) / 100n));
216
220
  };
217
221
  function assertNonNegativeInteger(name, value) {
218
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) panic(`${name} must be a finite non-negative integer`);
222
+ if (!Number.isSafeInteger(value) || value < 0) panic(`${name} must be a finite non-negative integer within the safe range`);
219
223
  }
224
+ const isCurrencyCents = (amount, currency) => currency !== "" && isMinorUnitAmount(amount);
220
225
  /**
221
226
  * Construct a `CurrencyCents<C>` from a currency code and a minor-unit
222
227
  * amount. The only producer of `CurrencyCents`; downstream code narrows `C`
@@ -225,7 +230,8 @@ function assertNonNegativeInteger(name, value) {
225
230
  */
226
231
  const currencyCents = (currency, amount) => {
227
232
  if (!currency) return panic("currencyCents(): currency must be a non-empty code");
228
- return cents(amount);
233
+ const minorUnits = cents(amount);
234
+ return isCurrencyCents(minorUnits, currency) ? minorUnits : panic("currencyCents(): currency must be a non-empty code");
229
235
  };
230
236
  /**
231
237
  * Add two `CurrencyCents` amounts of the SAME currency. The second
@@ -264,7 +270,7 @@ var MoneyTotals = class {
264
270
  * `localeCompare`, so ordering does not vary with the runtime's locale.
265
271
  */
266
272
  entries() {
267
- return [...this.#totals.keys()].sort().map((currency) => ({
273
+ return [...this.#totals.keys()].toSorted().map((currency) => ({
268
274
  currency,
269
275
  amountCents: this.#totals.get(currency) ?? cents(0)
270
276
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/money",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Branded minor-unit monetary amounts and currency-safe billing arithmetic.",
5
5
  "keywords": [
6
6
  "billing",
@@ -40,18 +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 --type-check packages/money",
44
44
  "lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/money",
45
45
  "format": "bun ../../scripts/run-oxfmt.ts .",
46
46
  "prepack": "bun run build"
47
47
  },
48
48
  "dependencies": {
49
- "better-result": "3.0.0"
49
+ "better-result": "3.0.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@stll/typescript-config": "0.0.0",
53
53
  "bun-types": "1.4.2",
54
- "tsdown": "0.22.14"
54
+ "tsdown": "0.23.0"
55
55
  },
56
56
  "main": "./dist/index.js",
57
57
  "types": "./dist/index.d.ts"