@trajectonomics/money 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +67 -2
- package/dist/index.cjs +133 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.mts +56 -0
- package/dist/index.mjs +130 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 excited.live
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @trajectonomics/money
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pure decimal scaling: major units to integer **Minor Units** and back, for any currency, with the currency precision supplied by the caller.
|
|
4
|
+
|
|
5
|
+
Zero dependencies, no I/O, no clock, no randomness. It ships no currency table and never touches `Intl`, so the same code serves a statutory tax engine that must stay locale-free and an accounting ledger that resolves ISO 4217 codes from the runtime.
|
|
6
|
+
|
|
7
|
+
## What this is not
|
|
8
|
+
|
|
9
|
+
- Not a currency authority: it does not validate or normalise currency codes, and it does not know how many digits a currency has. The caller supplies `digits`.
|
|
10
|
+
- Not money arithmetic: rounding, percentages, gross-ups, statutory caps and currency-tagged types belong to the packages that need them.
|
|
11
|
+
- Not signed-policy: whether a negative amount is acceptable is the caller's decision, expressed through `allowNegative`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @trajectonomics/money
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { fromMinorUnits, MoneyValidationError, toMinorUnits } from "@trajectonomics/money"
|
|
23
|
+
|
|
24
|
+
toMinorUnits(1.005, 2) // 101 - 101 cents, not 100: float noise never decides
|
|
25
|
+
toMinorUnits(1234, 0) // 1234 - JPY has no fractional digits
|
|
26
|
+
toMinorUnits(1.234, 3) // 1234 - KWD has three
|
|
27
|
+
toMinorUnits(-1.14, 2, "netTaxPayable", { allowNegative: true }) // -114
|
|
28
|
+
|
|
29
|
+
fromMinorUnits(1055, 2) // 10.55
|
|
30
|
+
fromMinorUnits(500, 0) // 500
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`toMinorUnits(amount, digits, field?, options?)` expands the amount's shortest decimal representation and rounds half away from zero, so the result never depends on binary floating-point multiplication. Scientific-notation input is scaled directly rather than mis-parsed.
|
|
34
|
+
|
|
35
|
+
`fromMinorUnits(minorUnits, digits, field?, options?)` is the inverse and also normalises `-0` to `+0`.
|
|
36
|
+
|
|
37
|
+
Both throw `MoneyValidationError` - carrying `field`, `value` and `reason` - when the precision is not a safe integer in `0..15`, the amount is `NaN` or infinite, a negative amount arrives without `allowNegative`, an amount is not an integer where one is required, or a result leaves the safe integer range.
|
|
38
|
+
|
|
39
|
+
## Crossing a package boundary
|
|
40
|
+
|
|
41
|
+
Convert **once**, at the boundary you own, and carry integer Minor Units from there:
|
|
42
|
+
|
|
43
|
+
- A value that came out of `fromMinorUnits` is canonical: it converts back to the same integer, so converting it once with any implementation is exact.
|
|
44
|
+
- A refund is negative. Pass `{ allowNegative: true }` when converting a net figure that can be below zero.
|
|
45
|
+
- Do not convert a value with one money implementation and re-convert it with another in the same flow; the diverging input classes are computed floats and amounts near the top of the safe integer range.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
- `bun run check`: lint, typecheck, tests with 100% line coverage, build, `publint --strict` and `attw`.
|
|
50
|
+
- `bun run test`: tests plus the coverage threshold.
|
|
51
|
+
- `bun run build`: ESM and CJS bundles via `tsdown`.
|
|
52
|
+
|
|
53
|
+
## Releasing
|
|
54
|
+
|
|
55
|
+
Versions are computed, never edited by hand (`package.json` says `0.0.0-development` on purpose). The shared publish workflow reads the last `vX.Y.Z` tag and the conventional commit messages since then:
|
|
56
|
+
|
|
57
|
+
| Commits since last release | Bump |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| any `feat!:` / `fix!:` / `BREAKING CHANGE:` | major (minor while on 0.x) |
|
|
60
|
+
| any `feat:` | minor |
|
|
61
|
+
| anything else | patch |
|
|
62
|
+
|
|
63
|
+
- **Alpha, automatic:** every merge to `main` that touches code publishes `<next>-alpha.<n>` under the npm dist-tag `alpha`.
|
|
64
|
+
- **Stable, on demand:** Actions -> Release -> Run workflow (on `main`) publishes `<next>` under `latest`, tags `vX.Y.Z` and creates a GitHub release.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when a monetary value violates a decimal-scaling invariant.
|
|
5
|
+
*
|
|
6
|
+
* Purpose: One failure type for both conversions, carrying the diagnostic field, the offending
|
|
7
|
+
* value and the reason, so each consumer can map it onto the error class its own API promises.
|
|
8
|
+
* Units: `value` is whatever the caller supplied - major units, integer Minor Units, or a digit count.
|
|
9
|
+
*/
|
|
10
|
+
var MoneyValidationError = class MoneyValidationError extends Error {
|
|
11
|
+
field;
|
|
12
|
+
value;
|
|
13
|
+
reason;
|
|
14
|
+
constructor(field, value, reason) {
|
|
15
|
+
super(`Money validation error on field '${field}': ${reason} (received: ${String(value)})`);
|
|
16
|
+
this.name = "MoneyValidationError";
|
|
17
|
+
this.field = field;
|
|
18
|
+
this.value = value;
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
Object.setPrototypeOf(this, MoneyValidationError.prototype);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Validates a currency precision supplied by the caller.
|
|
25
|
+
*
|
|
26
|
+
* Purpose: Rejects an out-of-range precision with a typed error instead of letting a raw
|
|
27
|
+
* `RangeError` escape from `toFixed` or a silent mis-scaling from `10 ** digits`.
|
|
28
|
+
* Math: Requires `Number.isSafeInteger(digits)` and `0 <= digits <= MAX_DIGITS`.
|
|
29
|
+
* Units: count of fractional decimal digits.
|
|
30
|
+
* Throws: MoneyValidationError when the count is fractional, negative, or above MAX_DIGITS.
|
|
31
|
+
*
|
|
32
|
+
* @param digits Fractional decimal digits of the currency, supplied by the caller.
|
|
33
|
+
* @param field Diagnostic field name reported on failure.
|
|
34
|
+
*/
|
|
35
|
+
function validateDigits(digits, field = "digits") {
|
|
36
|
+
if (!Number.isSafeInteger(digits)) throw new MoneyValidationError(field, digits, "Precision must be a safe integer");
|
|
37
|
+
if (digits < 0 || digits > 15) throw new MoneyValidationError(field, digits, `Precision must be between 0 and 15 digits`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validates a monetary input against numerical boundaries.
|
|
41
|
+
*
|
|
42
|
+
* Math: Rejects `NaN`, non-finite values and negatives unless `allowNegative` is set.
|
|
43
|
+
* Units: major units or integer Minor Units, depending on the caller.
|
|
44
|
+
* Throws: MoneyValidationError on any violation.
|
|
45
|
+
*
|
|
46
|
+
* @param value Amount to validate.
|
|
47
|
+
* @param field Diagnostic field name reported on failure.
|
|
48
|
+
* @param options Validation options.
|
|
49
|
+
*/
|
|
50
|
+
function validateMonetaryAmount(value, field, options) {
|
|
51
|
+
if (Number.isNaN(value)) throw new MoneyValidationError(field, value, "Value cannot be NaN");
|
|
52
|
+
if (!Number.isFinite(value)) throw new MoneyValidationError(field, value, "Value must be a finite number");
|
|
53
|
+
if (!options?.allowNegative && value < 0) throw new MoneyValidationError(field, value, "Value cannot be negative");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validates that an amount is an exact integer number of Minor Units.
|
|
57
|
+
*
|
|
58
|
+
* Math: Requires a finite, sign-allowed `Number.isSafeInteger`.
|
|
59
|
+
* Units: integer Minor Units.
|
|
60
|
+
* Throws: MoneyValidationError when the amount is not a safe integer.
|
|
61
|
+
*
|
|
62
|
+
* @param minorUnits Integer Minor Units to validate.
|
|
63
|
+
* @param field Diagnostic field name reported on failure.
|
|
64
|
+
* @param options Validation options.
|
|
65
|
+
*/
|
|
66
|
+
function validateIntegerMinorUnits(minorUnits, field, options) {
|
|
67
|
+
validateMonetaryAmount(minorUnits, field, options);
|
|
68
|
+
if (!Number.isSafeInteger(minorUnits)) throw new MoneyValidationError(field, minorUnits, "Minor units amount must be a safe integer");
|
|
69
|
+
}
|
|
70
|
+
// src/minor-units.ts
|
|
71
|
+
function scaleDecimalString(abs, precision) {
|
|
72
|
+
const factor = 10 ** precision;
|
|
73
|
+
if (String(abs).includes("e")) return abs * factor;
|
|
74
|
+
const parts = String(abs).split(".");
|
|
75
|
+
const intPart = parts[0] ?? "0";
|
|
76
|
+
const decPart = (parts[1] ?? "").padEnd(precision, "0");
|
|
77
|
+
const whole = intPart + decPart.slice(0, precision);
|
|
78
|
+
const fraction = decPart.slice(precision);
|
|
79
|
+
return Number(whole + (fraction.length > 0 ? `.${fraction}` : ""));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Converts a major-unit amount to integer Minor Units.
|
|
83
|
+
*
|
|
84
|
+
* Purpose: Exact major-to-Minor conversion for any currency, with precision supplied by the caller.
|
|
85
|
+
* Math: Expands the shortest decimal representation and rounds half away from zero, so binary
|
|
86
|
+
* float noise (`1.005 * 100`) never decides the result; scientific notation is scaled directly.
|
|
87
|
+
* Units: input in major units, output as integer Minor Units.
|
|
88
|
+
* Throws: MoneyValidationError when `digits` is out of range, the amount is `NaN`, infinite or
|
|
89
|
+
* negative without `allowNegative`, or the result leaves the safe integer range.
|
|
90
|
+
*
|
|
91
|
+
* @param amount Amount in major units.
|
|
92
|
+
* @param digits Fractional decimal digits of the currency (0 for JPY, 2 for USD, 3 for KWD).
|
|
93
|
+
* @param field Diagnostic field name reported on failure.
|
|
94
|
+
* @param options Validation options.
|
|
95
|
+
* @returns Amount as an integer number of Minor Units.
|
|
96
|
+
* @example toMinorUnits(1.005, 2) // 101 (101 cents, not 100)
|
|
97
|
+
*/
|
|
98
|
+
function toMinorUnits(amount, digits, field = "amount", options) {
|
|
99
|
+
validateDigits(digits);
|
|
100
|
+
validateMonetaryAmount(amount, field, options);
|
|
101
|
+
const sign = amount < 0 ? -1 : 1;
|
|
102
|
+
const scaled = scaleDecimalString(Math.abs(amount), digits);
|
|
103
|
+
const result = sign * Math.round(scaled) + 0;
|
|
104
|
+
if (!Number.isSafeInteger(result)) throw new MoneyValidationError(field, amount, "Conversion to minor units failed or exceeded safe integer bounds");
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Converts integer Minor Units back to a major-unit amount.
|
|
109
|
+
*
|
|
110
|
+
* Purpose: Inverse of `toMinorUnits`, producing the shortest decimal a double can carry.
|
|
111
|
+
* Math: Divides by `10 ** digits` and fixes the result to `digits` decimals, which also
|
|
112
|
+
* normalises `-0` to `+0`.
|
|
113
|
+
* Units: input as integer Minor Units, output in major units.
|
|
114
|
+
* Throws: MoneyValidationError when `digits` is out of range, or the input is not a safe integer.
|
|
115
|
+
*
|
|
116
|
+
* @param minorUnits Integer Minor Units.
|
|
117
|
+
* @param digits Fractional decimal digits of the currency.
|
|
118
|
+
* @param field Diagnostic field name reported on failure.
|
|
119
|
+
* @param options Validation options.
|
|
120
|
+
* @returns Amount in major units.
|
|
121
|
+
* @example fromMinorUnits(1055, 2) // 10.55
|
|
122
|
+
*/
|
|
123
|
+
function fromMinorUnits(minorUnits, digits, field = "minorUnits", options) {
|
|
124
|
+
validateDigits(digits);
|
|
125
|
+
validateIntegerMinorUnits(minorUnits, field, options);
|
|
126
|
+
const factor = 10 ** digits;
|
|
127
|
+
return (minorUnits < 0 ? -1 : 1) * Number((Math.abs(minorUnits) / factor).toFixed(digits)) + 0;
|
|
128
|
+
}
|
|
129
|
+
exports.MoneyValidationError = MoneyValidationError;
|
|
130
|
+
exports.fromMinorUnits = fromMinorUnits;
|
|
131
|
+
exports.toMinorUnits = toMinorUnits;
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/validate.ts","../src/minor-units.ts"],"sourcesContent":["/**\n * Error thrown when a monetary value violates a decimal-scaling invariant.\n *\n * Purpose: One failure type for both conversions, carrying the diagnostic field, the offending\n * value and the reason, so each consumer can map it onto the error class its own API promises.\n * Units: `value` is whatever the caller supplied - major units, integer Minor Units, or a digit count.\n */\nexport class MoneyValidationError extends Error {\n\treadonly field: string\n\treadonly value: unknown\n\treadonly reason: string\n\n\tconstructor(field: string, value: unknown, reason: string) {\n\t\tsuper(`Money validation error on field '${field}': ${reason} (received: ${String(value)})`)\n\t\tthis.name = \"MoneyValidationError\"\n\t\tthis.field = field\n\t\tthis.value = value\n\t\tthis.reason = reason\n\t\tObject.setPrototypeOf(this, MoneyValidationError.prototype)\n\t}\n}\n","import { MoneyValidationError } from \"./errors\"\n\n/**\n * Options controlling monetary validation.\n */\nexport interface ValidateAmountOptions {\n\t/** If true, negative values are accepted (e.g. a refund balance). */\n\treadonly allowNegative?: boolean\n}\n\n/**\n * Highest decimal precision the conversions accept.\n *\n * Math: `10 ** 15` is the largest power of ten a double holds exactly, so any larger precision\n * could not be scaled without losing digits.\n * Units: count of fractional decimal digits.\n */\nexport const MAX_DIGITS = 15\n\n/**\n * Validates a currency precision supplied by the caller.\n *\n * Purpose: Rejects an out-of-range precision with a typed error instead of letting a raw\n * `RangeError` escape from `toFixed` or a silent mis-scaling from `10 ** digits`.\n * Math: Requires `Number.isSafeInteger(digits)` and `0 <= digits <= MAX_DIGITS`.\n * Units: count of fractional decimal digits.\n * Throws: MoneyValidationError when the count is fractional, negative, or above MAX_DIGITS.\n *\n * @param digits Fractional decimal digits of the currency, supplied by the caller.\n * @param field Diagnostic field name reported on failure.\n */\nexport function validateDigits(digits: number, field = \"digits\"): void {\n\tif (!Number.isSafeInteger(digits)) {\n\t\tthrow new MoneyValidationError(field, digits, \"Precision must be a safe integer\")\n\t}\n\tif (digits < 0 || digits > MAX_DIGITS) {\n\t\tthrow new MoneyValidationError(\n\t\t\tfield,\n\t\t\tdigits,\n\t\t\t`Precision must be between 0 and ${MAX_DIGITS} digits`,\n\t\t)\n\t}\n}\n\n/**\n * Validates a monetary input against numerical boundaries.\n *\n * Math: Rejects `NaN`, non-finite values and negatives unless `allowNegative` is set.\n * Units: major units or integer Minor Units, depending on the caller.\n * Throws: MoneyValidationError on any violation.\n *\n * @param value Amount to validate.\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n */\nexport function validateMonetaryAmount(\n\tvalue: number,\n\tfield: string,\n\toptions?: ValidateAmountOptions,\n): void {\n\tif (Number.isNaN(value)) {\n\t\tthrow new MoneyValidationError(field, value, \"Value cannot be NaN\")\n\t}\n\tif (!Number.isFinite(value)) {\n\t\tthrow new MoneyValidationError(field, value, \"Value must be a finite number\")\n\t}\n\tif (!options?.allowNegative && value < 0) {\n\t\tthrow new MoneyValidationError(field, value, \"Value cannot be negative\")\n\t}\n}\n\n/**\n * Validates that an amount is an exact integer number of Minor Units.\n *\n * Math: Requires a finite, sign-allowed `Number.isSafeInteger`.\n * Units: integer Minor Units.\n * Throws: MoneyValidationError when the amount is not a safe integer.\n *\n * @param minorUnits Integer Minor Units to validate.\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n */\nexport function validateIntegerMinorUnits(\n\tminorUnits: number,\n\tfield: string,\n\toptions?: ValidateAmountOptions,\n): void {\n\tvalidateMonetaryAmount(minorUnits, field, options)\n\tif (!Number.isSafeInteger(minorUnits)) {\n\t\tthrow new MoneyValidationError(field, minorUnits, \"Minor units amount must be a safe integer\")\n\t}\n}\n","import { MoneyValidationError } from \"./errors\"\nimport {\n\ttype ValidateAmountOptions,\n\tvalidateDigits,\n\tvalidateIntegerMinorUnits,\n\tvalidateMonetaryAmount,\n} from \"./validate\"\n\nfunction scaleDecimalString(abs: number, precision: number): number {\n\tconst factor = 10 ** precision\n\tif (String(abs).includes(\"e\")) {\n\t\treturn abs * factor\n\t}\n\tconst parts = String(abs).split(\".\")\n\tconst intPart = parts[0] ?? \"0\"\n\tconst decPart = (parts[1] ?? \"\").padEnd(precision, \"0\")\n\tconst whole = intPart + decPart.slice(0, precision)\n\tconst fraction = decPart.slice(precision)\n\treturn Number(whole + (fraction.length > 0 ? `.${fraction}` : \"\"))\n}\n\n/**\n * Converts a major-unit amount to integer Minor Units.\n *\n * Purpose: Exact major-to-Minor conversion for any currency, with precision supplied by the caller.\n * Math: Expands the shortest decimal representation and rounds half away from zero, so binary\n * float noise (`1.005 * 100`) never decides the result; scientific notation is scaled directly.\n * Units: input in major units, output as integer Minor Units.\n * Throws: MoneyValidationError when `digits` is out of range, the amount is `NaN`, infinite or\n * negative without `allowNegative`, or the result leaves the safe integer range.\n *\n * @param amount Amount in major units.\n * @param digits Fractional decimal digits of the currency (0 for JPY, 2 for USD, 3 for KWD).\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n * @returns Amount as an integer number of Minor Units.\n * @example toMinorUnits(1.005, 2) // 101 (101 cents, not 100)\n */\nexport function toMinorUnits(\n\tamount: number,\n\tdigits: number,\n\tfield = \"amount\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateDigits(digits)\n\tvalidateMonetaryAmount(amount, field, options)\n\tconst sign = amount < 0 ? -1 : 1\n\tconst scaled = scaleDecimalString(Math.abs(amount), digits)\n\tconst result = sign * Math.round(scaled) + 0\n\tif (!Number.isSafeInteger(result)) {\n\t\tthrow new MoneyValidationError(\n\t\t\tfield,\n\t\t\tamount,\n\t\t\t\"Conversion to minor units failed or exceeded safe integer bounds\",\n\t\t)\n\t}\n\treturn result\n}\n\n/**\n * Converts integer Minor Units back to a major-unit amount.\n *\n * Purpose: Inverse of `toMinorUnits`, producing the shortest decimal a double can carry.\n * Math: Divides by `10 ** digits` and fixes the result to `digits` decimals, which also\n * normalises `-0` to `+0`.\n * Units: input as integer Minor Units, output in major units.\n * Throws: MoneyValidationError when `digits` is out of range, or the input is not a safe integer.\n *\n * @param minorUnits Integer Minor Units.\n * @param digits Fractional decimal digits of the currency.\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n * @returns Amount in major units.\n * @example fromMinorUnits(1055, 2) // 10.55\n */\nexport function fromMinorUnits(\n\tminorUnits: number,\n\tdigits: number,\n\tfield = \"minorUnits\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateDigits(digits)\n\tvalidateIntegerMinorUnits(minorUnits, field, options)\n\tconst factor = 10 ** digits\n\tconst sign = minorUnits < 0 ? -1 : 1\n\tconst abs = Math.abs(minorUnits)\n\treturn sign * Number((abs / factor).toFixed(digits)) + 0\n}\n"],"mappings":";;;;;;;;;AAOA,IAAa,uBAAb,MAAa,6BAA6B,MAAM;CAC/C;CACA;CACA;CAEA,YAAY,OAAe,OAAgB,QAAgB;EAC1D,MAAM,oCAAoC,MAAM,KAAK,OAAO,cAAc,OAAO,KAAK,EAAE,EAAE;EAC1F,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,SAAS;EACd,OAAO,eAAe,MAAM,qBAAqB,SAAS;CAC3D;AACD;;;;;;;;;;;;;ACWA,SAAgB,eAAe,QAAgB,QAAQ,UAAgB;CACtE,IAAI,CAAC,OAAO,cAAc,MAAM,GAC/B,MAAM,IAAI,qBAAqB,OAAO,QAAQ,kCAAkC;CAEjF,IAAI,SAAS,KAAK,SAAA,IACjB,MAAM,IAAI,qBACT,OACA,QACA,2CACD;AAEF;;;;;;;;;;;;AAaA,SAAgB,uBACf,OACA,OACA,SACO;CACP,IAAI,OAAO,MAAM,KAAK,GACrB,MAAM,IAAI,qBAAqB,OAAO,OAAO,qBAAqB;CAEnE,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,MAAM,IAAI,qBAAqB,OAAO,OAAO,+BAA+B;CAE7E,IAAI,CAAC,SAAS,iBAAiB,QAAQ,GACtC,MAAM,IAAI,qBAAqB,OAAO,OAAO,0BAA0B;AAEzE;;;;;;;;;;;;AAaA,SAAgB,0BACf,YACA,OACA,SACO;CACP,uBAAuB,YAAY,OAAO,OAAO;CACjD,IAAI,CAAC,OAAO,cAAc,UAAU,GACnC,MAAM,IAAI,qBAAqB,OAAO,YAAY,2CAA2C;AAE/F;;ACnFA,SAAS,mBAAmB,KAAa,WAA2B;CACnE,MAAM,SAAS,MAAM;CACrB,IAAI,OAAO,GAAG,CAAC,CAAC,SAAS,GAAG,GAC3B,OAAO,MAAM;CAEd,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,MAAM,GAAG;CACnC,MAAM,UAAU,MAAM,MAAM;CAC5B,MAAM,WAAW,MAAM,MAAM,GAAA,CAAI,OAAO,WAAW,GAAG;CACtD,MAAM,QAAQ,UAAU,QAAQ,MAAM,GAAG,SAAS;CAClD,MAAM,WAAW,QAAQ,MAAM,SAAS;CACxC,OAAO,OAAO,SAAS,SAAS,SAAS,IAAI,IAAI,aAAa,GAAG;AAClE;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,aACf,QACA,QACA,QAAQ,UACR,SACS;CACT,eAAe,MAAM;CACrB,uBAAuB,QAAQ,OAAO,OAAO;CAC7C,MAAM,OAAO,SAAS,IAAI,KAAK;CAC/B,MAAM,SAAS,mBAAmB,KAAK,IAAI,MAAM,GAAG,MAAM;CAC1D,MAAM,SAAS,OAAO,KAAK,MAAM,MAAM,IAAI;CAC3C,IAAI,CAAC,OAAO,cAAc,MAAM,GAC/B,MAAM,IAAI,qBACT,OACA,QACA,kEACD;CAED,OAAO;AACR;;;;;;;;;;;;;;;;;AAkBA,SAAgB,eACf,YACA,QACA,QAAQ,cACR,SACS;CACT,eAAe,MAAM;CACrB,0BAA0B,YAAY,OAAO,OAAO;CACpD,MAAM,SAAS,MAAM;CAGrB,QAFa,aAAa,IAAI,KAAK,KAErB,QADF,KAAK,IAAI,UACG,IAAI,OAAA,CAAQ,QAAQ,MAAM,CAAC,IAAI;AACxD"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a monetary value violates a decimal-scaling invariant.
|
|
3
|
+
*
|
|
4
|
+
* Purpose: One failure type for both conversions, carrying the diagnostic field, the offending
|
|
5
|
+
* value and the reason, so each consumer can map it onto the error class its own API promises.
|
|
6
|
+
* Units: `value` is whatever the caller supplied - major units, integer Minor Units, or a digit count.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MoneyValidationError extends Error {
|
|
9
|
+
readonly field: string;
|
|
10
|
+
readonly value: unknown;
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
constructor(field: string, value: unknown, reason: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Options controlling monetary validation.
|
|
16
|
+
*/
|
|
17
|
+
interface ValidateAmountOptions {
|
|
18
|
+
/** If true, negative values are accepted (e.g. a refund balance). */
|
|
19
|
+
readonly allowNegative?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Converts a major-unit amount to integer Minor Units.
|
|
23
|
+
*
|
|
24
|
+
* Purpose: Exact major-to-Minor conversion for any currency, with precision supplied by the caller.
|
|
25
|
+
* Math: Expands the shortest decimal representation and rounds half away from zero, so binary
|
|
26
|
+
* float noise (`1.005 * 100`) never decides the result; scientific notation is scaled directly.
|
|
27
|
+
* Units: input in major units, output as integer Minor Units.
|
|
28
|
+
* Throws: MoneyValidationError when `digits` is out of range, the amount is `NaN`, infinite or
|
|
29
|
+
* negative without `allowNegative`, or the result leaves the safe integer range.
|
|
30
|
+
*
|
|
31
|
+
* @param amount Amount in major units.
|
|
32
|
+
* @param digits Fractional decimal digits of the currency (0 for JPY, 2 for USD, 3 for KWD).
|
|
33
|
+
* @param field Diagnostic field name reported on failure.
|
|
34
|
+
* @param options Validation options.
|
|
35
|
+
* @returns Amount as an integer number of Minor Units.
|
|
36
|
+
* @example toMinorUnits(1.005, 2) // 101 (101 cents, not 100)
|
|
37
|
+
*/
|
|
38
|
+
export declare function toMinorUnits(amount: number, digits: number, field?: string, options?: ValidateAmountOptions): number;
|
|
39
|
+
/**
|
|
40
|
+
* Converts integer Minor Units back to a major-unit amount.
|
|
41
|
+
*
|
|
42
|
+
* Purpose: Inverse of `toMinorUnits`, producing the shortest decimal a double can carry.
|
|
43
|
+
* Math: Divides by `10 ** digits` and fixes the result to `digits` decimals, which also
|
|
44
|
+
* normalises `-0` to `+0`.
|
|
45
|
+
* Units: input as integer Minor Units, output in major units.
|
|
46
|
+
* Throws: MoneyValidationError when `digits` is out of range, or the input is not a safe integer.
|
|
47
|
+
*
|
|
48
|
+
* @param minorUnits Integer Minor Units.
|
|
49
|
+
* @param digits Fractional decimal digits of the currency.
|
|
50
|
+
* @param field Diagnostic field name reported on failure.
|
|
51
|
+
* @param options Validation options.
|
|
52
|
+
* @returns Amount in major units.
|
|
53
|
+
* @example fromMinorUnits(1055, 2) // 10.55
|
|
54
|
+
*/
|
|
55
|
+
export declare function fromMinorUnits(minorUnits: number, digits: number, field?: string, options?: ValidateAmountOptions): number;
|
|
56
|
+
export type { ValidateAmountOptions };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a monetary value violates a decimal-scaling invariant.
|
|
3
|
+
*
|
|
4
|
+
* Purpose: One failure type for both conversions, carrying the diagnostic field, the offending
|
|
5
|
+
* value and the reason, so each consumer can map it onto the error class its own API promises.
|
|
6
|
+
* Units: `value` is whatever the caller supplied - major units, integer Minor Units, or a digit count.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MoneyValidationError extends Error {
|
|
9
|
+
readonly field: string;
|
|
10
|
+
readonly value: unknown;
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
constructor(field: string, value: unknown, reason: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Options controlling monetary validation.
|
|
16
|
+
*/
|
|
17
|
+
interface ValidateAmountOptions {
|
|
18
|
+
/** If true, negative values are accepted (e.g. a refund balance). */
|
|
19
|
+
readonly allowNegative?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Converts a major-unit amount to integer Minor Units.
|
|
23
|
+
*
|
|
24
|
+
* Purpose: Exact major-to-Minor conversion for any currency, with precision supplied by the caller.
|
|
25
|
+
* Math: Expands the shortest decimal representation and rounds half away from zero, so binary
|
|
26
|
+
* float noise (`1.005 * 100`) never decides the result; scientific notation is scaled directly.
|
|
27
|
+
* Units: input in major units, output as integer Minor Units.
|
|
28
|
+
* Throws: MoneyValidationError when `digits` is out of range, the amount is `NaN`, infinite or
|
|
29
|
+
* negative without `allowNegative`, or the result leaves the safe integer range.
|
|
30
|
+
*
|
|
31
|
+
* @param amount Amount in major units.
|
|
32
|
+
* @param digits Fractional decimal digits of the currency (0 for JPY, 2 for USD, 3 for KWD).
|
|
33
|
+
* @param field Diagnostic field name reported on failure.
|
|
34
|
+
* @param options Validation options.
|
|
35
|
+
* @returns Amount as an integer number of Minor Units.
|
|
36
|
+
* @example toMinorUnits(1.005, 2) // 101 (101 cents, not 100)
|
|
37
|
+
*/
|
|
38
|
+
export declare function toMinorUnits(amount: number, digits: number, field?: string, options?: ValidateAmountOptions): number;
|
|
39
|
+
/**
|
|
40
|
+
* Converts integer Minor Units back to a major-unit amount.
|
|
41
|
+
*
|
|
42
|
+
* Purpose: Inverse of `toMinorUnits`, producing the shortest decimal a double can carry.
|
|
43
|
+
* Math: Divides by `10 ** digits` and fixes the result to `digits` decimals, which also
|
|
44
|
+
* normalises `-0` to `+0`.
|
|
45
|
+
* Units: input as integer Minor Units, output in major units.
|
|
46
|
+
* Throws: MoneyValidationError when `digits` is out of range, or the input is not a safe integer.
|
|
47
|
+
*
|
|
48
|
+
* @param minorUnits Integer Minor Units.
|
|
49
|
+
* @param digits Fractional decimal digits of the currency.
|
|
50
|
+
* @param field Diagnostic field name reported on failure.
|
|
51
|
+
* @param options Validation options.
|
|
52
|
+
* @returns Amount in major units.
|
|
53
|
+
* @example fromMinorUnits(1055, 2) // 10.55
|
|
54
|
+
*/
|
|
55
|
+
export declare function fromMinorUnits(minorUnits: number, digits: number, field?: string, options?: ValidateAmountOptions): number;
|
|
56
|
+
export type { ValidateAmountOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when a monetary value violates a decimal-scaling invariant.
|
|
4
|
+
*
|
|
5
|
+
* Purpose: One failure type for both conversions, carrying the diagnostic field, the offending
|
|
6
|
+
* value and the reason, so each consumer can map it onto the error class its own API promises.
|
|
7
|
+
* Units: `value` is whatever the caller supplied - major units, integer Minor Units, or a digit count.
|
|
8
|
+
*/
|
|
9
|
+
var MoneyValidationError = class MoneyValidationError extends Error {
|
|
10
|
+
field;
|
|
11
|
+
value;
|
|
12
|
+
reason;
|
|
13
|
+
constructor(field, value, reason) {
|
|
14
|
+
super(`Money validation error on field '${field}': ${reason} (received: ${String(value)})`);
|
|
15
|
+
this.name = "MoneyValidationError";
|
|
16
|
+
this.field = field;
|
|
17
|
+
this.value = value;
|
|
18
|
+
this.reason = reason;
|
|
19
|
+
Object.setPrototypeOf(this, MoneyValidationError.prototype);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Validates a currency precision supplied by the caller.
|
|
24
|
+
*
|
|
25
|
+
* Purpose: Rejects an out-of-range precision with a typed error instead of letting a raw
|
|
26
|
+
* `RangeError` escape from `toFixed` or a silent mis-scaling from `10 ** digits`.
|
|
27
|
+
* Math: Requires `Number.isSafeInteger(digits)` and `0 <= digits <= MAX_DIGITS`.
|
|
28
|
+
* Units: count of fractional decimal digits.
|
|
29
|
+
* Throws: MoneyValidationError when the count is fractional, negative, or above MAX_DIGITS.
|
|
30
|
+
*
|
|
31
|
+
* @param digits Fractional decimal digits of the currency, supplied by the caller.
|
|
32
|
+
* @param field Diagnostic field name reported on failure.
|
|
33
|
+
*/
|
|
34
|
+
function validateDigits(digits, field = "digits") {
|
|
35
|
+
if (!Number.isSafeInteger(digits)) throw new MoneyValidationError(field, digits, "Precision must be a safe integer");
|
|
36
|
+
if (digits < 0 || digits > 15) throw new MoneyValidationError(field, digits, `Precision must be between 0 and 15 digits`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validates a monetary input against numerical boundaries.
|
|
40
|
+
*
|
|
41
|
+
* Math: Rejects `NaN`, non-finite values and negatives unless `allowNegative` is set.
|
|
42
|
+
* Units: major units or integer Minor Units, depending on the caller.
|
|
43
|
+
* Throws: MoneyValidationError on any violation.
|
|
44
|
+
*
|
|
45
|
+
* @param value Amount to validate.
|
|
46
|
+
* @param field Diagnostic field name reported on failure.
|
|
47
|
+
* @param options Validation options.
|
|
48
|
+
*/
|
|
49
|
+
function validateMonetaryAmount(value, field, options) {
|
|
50
|
+
if (Number.isNaN(value)) throw new MoneyValidationError(field, value, "Value cannot be NaN");
|
|
51
|
+
if (!Number.isFinite(value)) throw new MoneyValidationError(field, value, "Value must be a finite number");
|
|
52
|
+
if (!options?.allowNegative && value < 0) throw new MoneyValidationError(field, value, "Value cannot be negative");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validates that an amount is an exact integer number of Minor Units.
|
|
56
|
+
*
|
|
57
|
+
* Math: Requires a finite, sign-allowed `Number.isSafeInteger`.
|
|
58
|
+
* Units: integer Minor Units.
|
|
59
|
+
* Throws: MoneyValidationError when the amount is not a safe integer.
|
|
60
|
+
*
|
|
61
|
+
* @param minorUnits Integer Minor Units to validate.
|
|
62
|
+
* @param field Diagnostic field name reported on failure.
|
|
63
|
+
* @param options Validation options.
|
|
64
|
+
*/
|
|
65
|
+
function validateIntegerMinorUnits(minorUnits, field, options) {
|
|
66
|
+
validateMonetaryAmount(minorUnits, field, options);
|
|
67
|
+
if (!Number.isSafeInteger(minorUnits)) throw new MoneyValidationError(field, minorUnits, "Minor units amount must be a safe integer");
|
|
68
|
+
}
|
|
69
|
+
// src/minor-units.ts
|
|
70
|
+
function scaleDecimalString(abs, precision) {
|
|
71
|
+
const factor = 10 ** precision;
|
|
72
|
+
if (String(abs).includes("e")) return abs * factor;
|
|
73
|
+
const parts = String(abs).split(".");
|
|
74
|
+
const intPart = parts[0] ?? "0";
|
|
75
|
+
const decPart = (parts[1] ?? "").padEnd(precision, "0");
|
|
76
|
+
const whole = intPart + decPart.slice(0, precision);
|
|
77
|
+
const fraction = decPart.slice(precision);
|
|
78
|
+
return Number(whole + (fraction.length > 0 ? `.${fraction}` : ""));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Converts a major-unit amount to integer Minor Units.
|
|
82
|
+
*
|
|
83
|
+
* Purpose: Exact major-to-Minor conversion for any currency, with precision supplied by the caller.
|
|
84
|
+
* Math: Expands the shortest decimal representation and rounds half away from zero, so binary
|
|
85
|
+
* float noise (`1.005 * 100`) never decides the result; scientific notation is scaled directly.
|
|
86
|
+
* Units: input in major units, output as integer Minor Units.
|
|
87
|
+
* Throws: MoneyValidationError when `digits` is out of range, the amount is `NaN`, infinite or
|
|
88
|
+
* negative without `allowNegative`, or the result leaves the safe integer range.
|
|
89
|
+
*
|
|
90
|
+
* @param amount Amount in major units.
|
|
91
|
+
* @param digits Fractional decimal digits of the currency (0 for JPY, 2 for USD, 3 for KWD).
|
|
92
|
+
* @param field Diagnostic field name reported on failure.
|
|
93
|
+
* @param options Validation options.
|
|
94
|
+
* @returns Amount as an integer number of Minor Units.
|
|
95
|
+
* @example toMinorUnits(1.005, 2) // 101 (101 cents, not 100)
|
|
96
|
+
*/
|
|
97
|
+
function toMinorUnits(amount, digits, field = "amount", options) {
|
|
98
|
+
validateDigits(digits);
|
|
99
|
+
validateMonetaryAmount(amount, field, options);
|
|
100
|
+
const sign = amount < 0 ? -1 : 1;
|
|
101
|
+
const scaled = scaleDecimalString(Math.abs(amount), digits);
|
|
102
|
+
const result = sign * Math.round(scaled) + 0;
|
|
103
|
+
if (!Number.isSafeInteger(result)) throw new MoneyValidationError(field, amount, "Conversion to minor units failed or exceeded safe integer bounds");
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Converts integer Minor Units back to a major-unit amount.
|
|
108
|
+
*
|
|
109
|
+
* Purpose: Inverse of `toMinorUnits`, producing the shortest decimal a double can carry.
|
|
110
|
+
* Math: Divides by `10 ** digits` and fixes the result to `digits` decimals, which also
|
|
111
|
+
* normalises `-0` to `+0`.
|
|
112
|
+
* Units: input as integer Minor Units, output in major units.
|
|
113
|
+
* Throws: MoneyValidationError when `digits` is out of range, or the input is not a safe integer.
|
|
114
|
+
*
|
|
115
|
+
* @param minorUnits Integer Minor Units.
|
|
116
|
+
* @param digits Fractional decimal digits of the currency.
|
|
117
|
+
* @param field Diagnostic field name reported on failure.
|
|
118
|
+
* @param options Validation options.
|
|
119
|
+
* @returns Amount in major units.
|
|
120
|
+
* @example fromMinorUnits(1055, 2) // 10.55
|
|
121
|
+
*/
|
|
122
|
+
function fromMinorUnits(minorUnits, digits, field = "minorUnits", options) {
|
|
123
|
+
validateDigits(digits);
|
|
124
|
+
validateIntegerMinorUnits(minorUnits, field, options);
|
|
125
|
+
const factor = 10 ** digits;
|
|
126
|
+
return (minorUnits < 0 ? -1 : 1) * Number((Math.abs(minorUnits) / factor).toFixed(digits)) + 0;
|
|
127
|
+
}
|
|
128
|
+
export { MoneyValidationError, fromMinorUnits, toMinorUnits };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/errors.ts","../src/validate.ts","../src/minor-units.ts"],"sourcesContent":["/**\n * Error thrown when a monetary value violates a decimal-scaling invariant.\n *\n * Purpose: One failure type for both conversions, carrying the diagnostic field, the offending\n * value and the reason, so each consumer can map it onto the error class its own API promises.\n * Units: `value` is whatever the caller supplied - major units, integer Minor Units, or a digit count.\n */\nexport class MoneyValidationError extends Error {\n\treadonly field: string\n\treadonly value: unknown\n\treadonly reason: string\n\n\tconstructor(field: string, value: unknown, reason: string) {\n\t\tsuper(`Money validation error on field '${field}': ${reason} (received: ${String(value)})`)\n\t\tthis.name = \"MoneyValidationError\"\n\t\tthis.field = field\n\t\tthis.value = value\n\t\tthis.reason = reason\n\t\tObject.setPrototypeOf(this, MoneyValidationError.prototype)\n\t}\n}\n","import { MoneyValidationError } from \"./errors\"\n\n/**\n * Options controlling monetary validation.\n */\nexport interface ValidateAmountOptions {\n\t/** If true, negative values are accepted (e.g. a refund balance). */\n\treadonly allowNegative?: boolean\n}\n\n/**\n * Highest decimal precision the conversions accept.\n *\n * Math: `10 ** 15` is the largest power of ten a double holds exactly, so any larger precision\n * could not be scaled without losing digits.\n * Units: count of fractional decimal digits.\n */\nexport const MAX_DIGITS = 15\n\n/**\n * Validates a currency precision supplied by the caller.\n *\n * Purpose: Rejects an out-of-range precision with a typed error instead of letting a raw\n * `RangeError` escape from `toFixed` or a silent mis-scaling from `10 ** digits`.\n * Math: Requires `Number.isSafeInteger(digits)` and `0 <= digits <= MAX_DIGITS`.\n * Units: count of fractional decimal digits.\n * Throws: MoneyValidationError when the count is fractional, negative, or above MAX_DIGITS.\n *\n * @param digits Fractional decimal digits of the currency, supplied by the caller.\n * @param field Diagnostic field name reported on failure.\n */\nexport function validateDigits(digits: number, field = \"digits\"): void {\n\tif (!Number.isSafeInteger(digits)) {\n\t\tthrow new MoneyValidationError(field, digits, \"Precision must be a safe integer\")\n\t}\n\tif (digits < 0 || digits > MAX_DIGITS) {\n\t\tthrow new MoneyValidationError(\n\t\t\tfield,\n\t\t\tdigits,\n\t\t\t`Precision must be between 0 and ${MAX_DIGITS} digits`,\n\t\t)\n\t}\n}\n\n/**\n * Validates a monetary input against numerical boundaries.\n *\n * Math: Rejects `NaN`, non-finite values and negatives unless `allowNegative` is set.\n * Units: major units or integer Minor Units, depending on the caller.\n * Throws: MoneyValidationError on any violation.\n *\n * @param value Amount to validate.\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n */\nexport function validateMonetaryAmount(\n\tvalue: number,\n\tfield: string,\n\toptions?: ValidateAmountOptions,\n): void {\n\tif (Number.isNaN(value)) {\n\t\tthrow new MoneyValidationError(field, value, \"Value cannot be NaN\")\n\t}\n\tif (!Number.isFinite(value)) {\n\t\tthrow new MoneyValidationError(field, value, \"Value must be a finite number\")\n\t}\n\tif (!options?.allowNegative && value < 0) {\n\t\tthrow new MoneyValidationError(field, value, \"Value cannot be negative\")\n\t}\n}\n\n/**\n * Validates that an amount is an exact integer number of Minor Units.\n *\n * Math: Requires a finite, sign-allowed `Number.isSafeInteger`.\n * Units: integer Minor Units.\n * Throws: MoneyValidationError when the amount is not a safe integer.\n *\n * @param minorUnits Integer Minor Units to validate.\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n */\nexport function validateIntegerMinorUnits(\n\tminorUnits: number,\n\tfield: string,\n\toptions?: ValidateAmountOptions,\n): void {\n\tvalidateMonetaryAmount(minorUnits, field, options)\n\tif (!Number.isSafeInteger(minorUnits)) {\n\t\tthrow new MoneyValidationError(field, minorUnits, \"Minor units amount must be a safe integer\")\n\t}\n}\n","import { MoneyValidationError } from \"./errors\"\nimport {\n\ttype ValidateAmountOptions,\n\tvalidateDigits,\n\tvalidateIntegerMinorUnits,\n\tvalidateMonetaryAmount,\n} from \"./validate\"\n\nfunction scaleDecimalString(abs: number, precision: number): number {\n\tconst factor = 10 ** precision\n\tif (String(abs).includes(\"e\")) {\n\t\treturn abs * factor\n\t}\n\tconst parts = String(abs).split(\".\")\n\tconst intPart = parts[0] ?? \"0\"\n\tconst decPart = (parts[1] ?? \"\").padEnd(precision, \"0\")\n\tconst whole = intPart + decPart.slice(0, precision)\n\tconst fraction = decPart.slice(precision)\n\treturn Number(whole + (fraction.length > 0 ? `.${fraction}` : \"\"))\n}\n\n/**\n * Converts a major-unit amount to integer Minor Units.\n *\n * Purpose: Exact major-to-Minor conversion for any currency, with precision supplied by the caller.\n * Math: Expands the shortest decimal representation and rounds half away from zero, so binary\n * float noise (`1.005 * 100`) never decides the result; scientific notation is scaled directly.\n * Units: input in major units, output as integer Minor Units.\n * Throws: MoneyValidationError when `digits` is out of range, the amount is `NaN`, infinite or\n * negative without `allowNegative`, or the result leaves the safe integer range.\n *\n * @param amount Amount in major units.\n * @param digits Fractional decimal digits of the currency (0 for JPY, 2 for USD, 3 for KWD).\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n * @returns Amount as an integer number of Minor Units.\n * @example toMinorUnits(1.005, 2) // 101 (101 cents, not 100)\n */\nexport function toMinorUnits(\n\tamount: number,\n\tdigits: number,\n\tfield = \"amount\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateDigits(digits)\n\tvalidateMonetaryAmount(amount, field, options)\n\tconst sign = amount < 0 ? -1 : 1\n\tconst scaled = scaleDecimalString(Math.abs(amount), digits)\n\tconst result = sign * Math.round(scaled) + 0\n\tif (!Number.isSafeInteger(result)) {\n\t\tthrow new MoneyValidationError(\n\t\t\tfield,\n\t\t\tamount,\n\t\t\t\"Conversion to minor units failed or exceeded safe integer bounds\",\n\t\t)\n\t}\n\treturn result\n}\n\n/**\n * Converts integer Minor Units back to a major-unit amount.\n *\n * Purpose: Inverse of `toMinorUnits`, producing the shortest decimal a double can carry.\n * Math: Divides by `10 ** digits` and fixes the result to `digits` decimals, which also\n * normalises `-0` to `+0`.\n * Units: input as integer Minor Units, output in major units.\n * Throws: MoneyValidationError when `digits` is out of range, or the input is not a safe integer.\n *\n * @param minorUnits Integer Minor Units.\n * @param digits Fractional decimal digits of the currency.\n * @param field Diagnostic field name reported on failure.\n * @param options Validation options.\n * @returns Amount in major units.\n * @example fromMinorUnits(1055, 2) // 10.55\n */\nexport function fromMinorUnits(\n\tminorUnits: number,\n\tdigits: number,\n\tfield = \"minorUnits\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateDigits(digits)\n\tvalidateIntegerMinorUnits(minorUnits, field, options)\n\tconst factor = 10 ** digits\n\tconst sign = minorUnits < 0 ? -1 : 1\n\tconst abs = Math.abs(minorUnits)\n\treturn sign * Number((abs / factor).toFixed(digits)) + 0\n}\n"],"mappings":";;;;;;;;AAOA,IAAa,uBAAb,MAAa,6BAA6B,MAAM;CAC/C;CACA;CACA;CAEA,YAAY,OAAe,OAAgB,QAAgB;EAC1D,MAAM,oCAAoC,MAAM,KAAK,OAAO,cAAc,OAAO,KAAK,EAAE,EAAE;EAC1F,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,SAAS;EACd,OAAO,eAAe,MAAM,qBAAqB,SAAS;CAC3D;AACD;;;;;;;;;;;;;ACWA,SAAgB,eAAe,QAAgB,QAAQ,UAAgB;CACtE,IAAI,CAAC,OAAO,cAAc,MAAM,GAC/B,MAAM,IAAI,qBAAqB,OAAO,QAAQ,kCAAkC;CAEjF,IAAI,SAAS,KAAK,SAAA,IACjB,MAAM,IAAI,qBACT,OACA,QACA,2CACD;AAEF;;;;;;;;;;;;AAaA,SAAgB,uBACf,OACA,OACA,SACO;CACP,IAAI,OAAO,MAAM,KAAK,GACrB,MAAM,IAAI,qBAAqB,OAAO,OAAO,qBAAqB;CAEnE,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,MAAM,IAAI,qBAAqB,OAAO,OAAO,+BAA+B;CAE7E,IAAI,CAAC,SAAS,iBAAiB,QAAQ,GACtC,MAAM,IAAI,qBAAqB,OAAO,OAAO,0BAA0B;AAEzE;;;;;;;;;;;;AAaA,SAAgB,0BACf,YACA,OACA,SACO;CACP,uBAAuB,YAAY,OAAO,OAAO;CACjD,IAAI,CAAC,OAAO,cAAc,UAAU,GACnC,MAAM,IAAI,qBAAqB,OAAO,YAAY,2CAA2C;AAE/F;;ACnFA,SAAS,mBAAmB,KAAa,WAA2B;CACnE,MAAM,SAAS,MAAM;CACrB,IAAI,OAAO,GAAG,CAAC,CAAC,SAAS,GAAG,GAC3B,OAAO,MAAM;CAEd,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,MAAM,GAAG;CACnC,MAAM,UAAU,MAAM,MAAM;CAC5B,MAAM,WAAW,MAAM,MAAM,GAAA,CAAI,OAAO,WAAW,GAAG;CACtD,MAAM,QAAQ,UAAU,QAAQ,MAAM,GAAG,SAAS;CAClD,MAAM,WAAW,QAAQ,MAAM,SAAS;CACxC,OAAO,OAAO,SAAS,SAAS,SAAS,IAAI,IAAI,aAAa,GAAG;AAClE;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,aACf,QACA,QACA,QAAQ,UACR,SACS;CACT,eAAe,MAAM;CACrB,uBAAuB,QAAQ,OAAO,OAAO;CAC7C,MAAM,OAAO,SAAS,IAAI,KAAK;CAC/B,MAAM,SAAS,mBAAmB,KAAK,IAAI,MAAM,GAAG,MAAM;CAC1D,MAAM,SAAS,OAAO,KAAK,MAAM,MAAM,IAAI;CAC3C,IAAI,CAAC,OAAO,cAAc,MAAM,GAC/B,MAAM,IAAI,qBACT,OACA,QACA,kEACD;CAED,OAAO;AACR;;;;;;;;;;;;;;;;;AAkBA,SAAgB,eACf,YACA,QACA,QAAQ,cACR,SACS;CACT,eAAe,MAAM;CACrB,0BAA0B,YAAY,OAAO,OAAO;CACpD,MAAM,SAAS,MAAM;CAGrB,QAFa,aAAa,IAAI,KAAK,KAErB,QADF,KAAK,IAAI,UACG,IAAI,OAAA,CAAQ,QAAQ,MAAM,CAAC,IAAI;AACxD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@trajectonomics/money",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Pure decimal scaling: major units to integer Minor Units and back, with precision supplied by the caller.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/trajectonomics/money.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"packageManager": "bun@1.4.4",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.mjs",
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"test": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text && bun run scripts/check-coverage.ts",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
|
|
43
|
+
"lint": "biome ci --error-on-warnings .",
|
|
44
|
+
"format": "biome check --write .",
|
|
45
|
+
"check": "bun run lint && bun run typecheck && bun run test && bun run build && bun run lint:package",
|
|
46
|
+
"lint:package": "publint --strict && attw --pack . --profile node16"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
50
|
+
"@biomejs/biome": "2.6.3",
|
|
51
|
+
"@trajectonomics/tooling": "1.1.0",
|
|
52
|
+
"@types/bun": "^1.4.4",
|
|
53
|
+
"publint": "^0.3.25",
|
|
54
|
+
"tsdown": "^0.23.2",
|
|
55
|
+
"typescript": "^7.0.2"
|
|
56
|
+
}
|
|
57
|
+
}
|