@azghr/specie 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-07-22
4
+
5
+ ### Added
6
+ - Complete Money implementation with integer-cents arithmetic
7
+ - Currency-aware formatting via Intl.NumberFormat
8
+ - Largest-remainder allocation algorithm for exact bill splitting
9
+ - Half-even (banker's), half-up, down, and up rounding modes
10
+ - Safe integer validation and overflow protection
11
+ - Currency mismatch protection with CurrencyMismatch error
12
+ - Comprehensive test suite with 54 tests covering all invariants and edge cases
13
+ - Runnable demo with printed evidence of package claims
14
+
15
+ ### Fixed
16
+ - fromDecimal now correctly uses getCurrencyFractionDigits instead of hardcoded value
17
+ - Negative decimal string parsing ("-10.50" → -1050 cents)
18
+ - Multiply overflow protection (caps at MAX_SAFE_INTEGER/MIN_SAFE_INTEGER)
19
+
20
+ ### Documentation
21
+ - Complete README with API documentation, usage examples, and non-goals
22
+ - Seven-section demo showing float bug prevention, allocation, rounding, and formatting
23
+ - TypeScript examples for full type safety
24
+
25
+ ## 0.1.0 (2026-07-22)
26
+
27
+ ### Added
28
+ - Initial package scaffold with TypeScript, ESLint, and Vitest configuration
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 npmdevkit authors
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 ADDED
@@ -0,0 +1,127 @@
1
+ # specie
2
+
3
+ Tiny integer-cents money type — safe arithmetic, currency-aware rounding, Intl formatting. No bignum, no dependencies.
4
+
5
+ ## The Problem
6
+
7
+ `0.1 + 0.2 !== 0.3`. Storing money as floats produces rounding errors and audit nightmares. Correct money is integer minor units + a currency + explicit rounding + locale formatting.
8
+
9
+ This is competitive space: dinero.js, big.js, and decimal.js cover arbitrary decimals. specie is worth building only because it stays radically smaller: integer minor units only, currency-aware default fractions via Intl, and a few explicit operations.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @azghr/specie
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```typescript
20
+ import { money, fromDecimal } from "@azghr/specie";
21
+
22
+ // 10-second copy-paste
23
+ const price = money(1999, "USD"); // $19.99 as 1999 cents
24
+ price.add(money(100, "USD")).format(); // "$20.99"
25
+
26
+ // Realistic usage
27
+ const subtotal = fromDecimal("19.99", "USD");
28
+ const tax = subtotal.multiply(0.0825); // 8.25% tax
29
+ const total = subtotal.add(tax);
30
+ total.format(); // "$21.64"
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### `money(amount: number, currency: string): Money`
36
+ Create Money from integer minor units. `amount` must be a safe integer.
37
+
38
+ ```typescript
39
+ money(1999, "USD") // $19.99
40
+ money(500, "JPY") // ¥500
41
+ ```
42
+
43
+ ### `fromDecimal(value: string, currency: string): Money`
44
+ Parse decimal string to Money. Never uses float parsing to avoid rounding errors.
45
+
46
+ ```typescript
47
+ fromDecimal("19.99", "USD") // 1999 cents
48
+ fromDecimal("-10.50", "USD") // -1050 cents
49
+ ```
50
+
51
+ ### `Money.add(other: Money): Money`
52
+ Add another Money value (same currency only). Throws `CurrencyMismatch` on currency mismatch.
53
+
54
+ ### `Money.subtract(other: Money): Money`
55
+ Subtract another Money value (same currency only).
56
+
57
+ ### `Money.multiply(factor: number, rounding?: Rounding): Money`
58
+ Multiply by a factor with rounding. Default: `"half-even"` (banker's rounding).
59
+
60
+ ```typescript
61
+ money(1000, "USD").multiply(1.0825) // Apply 8.25% tax
62
+ money(100, "USD").multiply(1.5, "half-up") // Force ties up
63
+ ```
64
+
65
+ ### `Money.allocate(ratios: readonly number[]): Money[]`
66
+ Distribute by ratios using largest-remainder method. Parts sum exactly to the original amount.
67
+
68
+ ```typescript
69
+ money(100, "USD").allocate([3, 2]) // [$60, $40] (not $59.99/$40.01)
70
+ ```
71
+
72
+ ### `Money.format(locale?: string): string`
73
+ Format using `Intl.NumberFormat`.
74
+
75
+ ```typescript
76
+ money(1999, "USD").format() // "$19.99"
77
+ money(123456, "JPY").format("ja-JP") // "¥123,456" (no decimals)
78
+ ```
79
+
80
+ ### `Money.compare(other: Money): -1 | 0 | 1`
81
+ Compare with another Money. Throws `CurrencyMismatch` on currency mismatch.
82
+
83
+ ### `Money.equals(other: Money): boolean`
84
+ Check equality.
85
+
86
+ ### `Money.isZero()`, `Money.isNegative()`, `Money.isPositive()`
87
+ Check amount sign.
88
+
89
+ ### `Money.toJSON(): { amount: number; currency: string }`
90
+ Serialize to plain object.
91
+
92
+ ### Type: `Rounding`
93
+
94
+ ```typescript
95
+ "half-up" | "half-even" | "down" | "up"
96
+ ```
97
+
98
+ ### Class: `CurrencyMismatch`
99
+
100
+ Error thrown when operations mix different currencies.
101
+
102
+ ## Non-Goals
103
+
104
+ Use dinero.js or decimal.js for:
105
+ - Arbitrary precision / values beyond `Number.MAX_SAFE_INTEGER`
106
+ - Multi-currency conversion / forex
107
+ - Historical rates
108
+ - Crypto amounts with 18 decimals
109
+
110
+ ## TypeScript Note
111
+
112
+ ```typescript
113
+ import { money, fromDecimal } from "@azghr/specie";
114
+
115
+ // Full type safety
116
+ const price: Money = money(1999, "USD");
117
+ const total: Money = price.add(money(500, "USD"));
118
+
119
+ // Currency mismatch caught at compile time
120
+ const usd = money(100, "USD");
121
+ const eur = money(100, "EUR");
122
+ // usd.add(eur) // TypeScript: this works, runtime throws CurrencyMismatch
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CurrencyMismatch: () => CurrencyMismatch,
24
+ default: () => money,
25
+ fromDecimal: () => fromDecimal,
26
+ money: () => money
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/format.ts
31
+ var FRACTION_DIGITS = {
32
+ JPY: 0,
33
+ USD: 2,
34
+ EUR: 2,
35
+ GBP: 2,
36
+ CAD: 2,
37
+ AUD: 2,
38
+ CHF: 2,
39
+ CNY: 2,
40
+ INR: 2
41
+ };
42
+ function getCurrencyFractionDigits(currency) {
43
+ try {
44
+ const formatter = new Intl.NumberFormat("en-US", {
45
+ style: "currency",
46
+ currency
47
+ });
48
+ const resolved = formatter.resolvedOptions();
49
+ if (resolved.fractionDigits !== void 0) {
50
+ return resolved.fractionDigits;
51
+ }
52
+ if (resolved.minimumFractionDigits !== void 0) {
53
+ return resolved.minimumFractionDigits;
54
+ }
55
+ } catch {
56
+ }
57
+ return FRACTION_DIGITS[currency] ?? 2;
58
+ }
59
+ function formatMoney(amount, currency, locale = "en-US") {
60
+ const fractionDigits = getCurrencyFractionDigits(currency);
61
+ const divisor = Math.pow(10, fractionDigits);
62
+ const majorAmount = amount / divisor;
63
+ const formatter = new Intl.NumberFormat(locale, {
64
+ style: "currency",
65
+ currency,
66
+ minimumFractionDigits: fractionDigits,
67
+ maximumFractionDigits: fractionDigits
68
+ });
69
+ return formatter.format(majorAmount);
70
+ }
71
+
72
+ // src/allocate.ts
73
+ function allocate(totalAmount, ratios) {
74
+ if (ratios.length === 0) {
75
+ throw new RangeError("Cannot allocate with empty ratios");
76
+ }
77
+ const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0);
78
+ if (totalRatio <= 0) {
79
+ throw new RangeError("Ratios must sum to a positive value");
80
+ }
81
+ const allocations = [];
82
+ const remainders = [];
83
+ let distributed = 0;
84
+ for (let i = 0; i < ratios.length; i++) {
85
+ const ratio = ratios[i];
86
+ if (ratio === void 0) continue;
87
+ const exact = totalAmount * ratio / totalRatio;
88
+ const floored = Math.floor(exact);
89
+ allocations.push(floored);
90
+ remainders.push({ index: i, remainder: exact - floored });
91
+ distributed += floored;
92
+ }
93
+ const remainder = totalAmount - distributed;
94
+ const sortedByRemainder = remainders.map((r, index) => ({ ...r, originalIndex: index })).sort((a, b) => {
95
+ if (b.remainder !== a.remainder) {
96
+ return b.remainder - a.remainder;
97
+ }
98
+ return a.originalIndex - b.originalIndex;
99
+ });
100
+ for (let i = 0; i < remainder && i < sortedByRemainder.length; i++) {
101
+ const item = sortedByRemainder[i];
102
+ if (item) {
103
+ const idx = item.originalIndex;
104
+ allocations[idx] = (allocations[idx] ?? 0) + 1;
105
+ }
106
+ }
107
+ return allocations;
108
+ }
109
+
110
+ // src/errors.ts
111
+ var CurrencyMismatch = class extends Error {
112
+ constructor() {
113
+ super("Currency mismatch: operations require matching currencies");
114
+ this.name = "CurrencyMismatch";
115
+ }
116
+ };
117
+
118
+ // src/money.ts
119
+ var MoneyImpl = class _MoneyImpl {
120
+ constructor(amount, currency) {
121
+ this.amount = amount;
122
+ this.currency = currency;
123
+ if (!Number.isSafeInteger(amount)) {
124
+ throw new RangeError(
125
+ `Amount must be a safe integer, got: ${String(amount)}`
126
+ );
127
+ }
128
+ }
129
+ amount;
130
+ currency;
131
+ add(other) {
132
+ this.ensureSameCurrency(other);
133
+ return new _MoneyImpl(this.amount + other.amount, this.currency);
134
+ }
135
+ subtract(other) {
136
+ this.ensureSameCurrency(other);
137
+ return new _MoneyImpl(this.amount - other.amount, this.currency);
138
+ }
139
+ multiply(factor, rounding = "half-even") {
140
+ const exact = this.amount * factor;
141
+ const rounded = this.applyRounding(exact, rounding);
142
+ if (rounded > Number.MAX_SAFE_INTEGER) {
143
+ return new _MoneyImpl(Number.MAX_SAFE_INTEGER, this.currency);
144
+ }
145
+ if (rounded < Number.MIN_SAFE_INTEGER) {
146
+ return new _MoneyImpl(Number.MIN_SAFE_INTEGER, this.currency);
147
+ }
148
+ return new _MoneyImpl(rounded, this.currency);
149
+ }
150
+ allocate(ratios) {
151
+ const allocations = allocate(this.amount, ratios);
152
+ return allocations.map((amount) => new _MoneyImpl(amount, this.currency));
153
+ }
154
+ compare(other) {
155
+ this.ensureSameCurrency(other);
156
+ if (this.amount < other.amount) return -1;
157
+ if (this.amount > other.amount) return 1;
158
+ return 0;
159
+ }
160
+ equals(other) {
161
+ return this.compare(other) === 0;
162
+ }
163
+ isZero() {
164
+ return this.amount === 0;
165
+ }
166
+ isNegative() {
167
+ return this.amount < 0;
168
+ }
169
+ isPositive() {
170
+ return this.amount > 0;
171
+ }
172
+ format(locale = "en-US") {
173
+ return formatMoney(this.amount, this.currency, locale);
174
+ }
175
+ toJSON() {
176
+ return { amount: this.amount, currency: this.currency };
177
+ }
178
+ ensureSameCurrency(other) {
179
+ if (this.currency !== other.currency) {
180
+ throw new CurrencyMismatch();
181
+ }
182
+ }
183
+ applyRounding(value, mode) {
184
+ if (Number.isSafeInteger(value)) {
185
+ return value;
186
+ }
187
+ switch (mode) {
188
+ case "half-up": {
189
+ const fractional = value - Math.floor(value);
190
+ if (fractional === 0.5) {
191
+ return Math.ceil(value);
192
+ }
193
+ return Math.round(value);
194
+ }
195
+ case "half-even": {
196
+ const rounded = Math.round(value);
197
+ const isHalf = Math.abs(value - rounded) === 0.5;
198
+ if (isHalf) {
199
+ return Math.round(value / 2) * 2;
200
+ }
201
+ return rounded;
202
+ }
203
+ case "down":
204
+ return Math.floor(value);
205
+ case "up":
206
+ return Math.ceil(value);
207
+ default:
208
+ return Math.round(value);
209
+ }
210
+ }
211
+ };
212
+ function money(amount, currency) {
213
+ return new MoneyImpl(amount, currency);
214
+ }
215
+ function fromDecimal(value, currency) {
216
+ const fractionDigits = getCurrencyFractionDigits(currency);
217
+ const parts = value.split(".");
218
+ if (parts.length > 2) {
219
+ throw new RangeError(`Invalid decimal format: ${value}`);
220
+ }
221
+ const integerPart = parts[0] || "0";
222
+ const fractionalPart = parts[1] || "";
223
+ const isNegative = integerPart.startsWith("-");
224
+ const absIntegerPart = isNegative ? integerPart.slice(1) : integerPart;
225
+ if (absIntegerPart === "0" && (fractionalPart === "" || fractionalPart === "0")) {
226
+ return money(0, currency);
227
+ }
228
+ if (fractionalPart.length > fractionDigits) {
229
+ const hasExtraDigits = fractionalPart.slice(fractionDigits).split("").some((d) => d !== "0");
230
+ if (hasExtraDigits) {
231
+ throw new RangeError(
232
+ `Too many fraction digits for ${currency}: ${value} (max ${String(fractionDigits)})`
233
+ );
234
+ }
235
+ }
236
+ const majorUnits = parseInt(absIntegerPart, 10);
237
+ let minorUnits = 0;
238
+ if (fractionalPart.length > 0) {
239
+ const padded = fractionalPart.padEnd(fractionDigits, "0");
240
+ minorUnits = parseInt(padded, 10);
241
+ }
242
+ const totalAmount = majorUnits * Math.pow(10, fractionDigits) + minorUnits;
243
+ return money(isNegative ? -totalAmount : totalAmount, currency);
244
+ }
245
+ // Annotate the CommonJS export names for ESM import in node:
246
+ 0 && (module.exports = {
247
+ CurrencyMismatch,
248
+ fromDecimal,
249
+ money
250
+ });
251
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/format.ts","../src/allocate.ts","../src/errors.ts","../src/money.ts"],"sourcesContent":["/**\n * specie - Tiny integer-cents money type\n *\n * Safe arithmetic, currency-aware rounding, Intl formatting.\n * No bignum, no dependencies.\n */\n\n// Re-export everything from money.ts (main module)\nexport {\n money,\n fromDecimal,\n type Money,\n type Rounding,\n CurrencyMismatch,\n} from \"./money.js\";\n\n// Default export\nexport { money as default } from \"./money.js\";\n","/**\n * Currency fraction digits fallback table for common currencies.\n * Used when Intl.NumberFormat is not available or returns unexpected results.\n */\nconst FRACTION_DIGITS: Record<string, number> = {\n JPY: 0,\n USD: 2,\n EUR: 2,\n GBP: 2,\n CAD: 2,\n AUD: 2,\n CHF: 2,\n CNY: 2,\n INR: 2,\n};\n\n/**\n * Get the number of fraction digits for a currency code.\n * Uses Intl.NumberFormat if available, with fallback table.\n *\n * @param currency - ISO 4217 currency code\n * @returns Number of fraction digits (usually 0 or 2)\n */\nexport function getCurrencyFractionDigits(currency: string): number {\n try {\n // Try to get from Intl\n const formatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency,\n });\n\n const resolved =\n formatter.resolvedOptions() as Intl.ResolvedNumberFormatOptions & {\n fractionDigits?: number;\n minimumFractionDigits?: number;\n maximumFractionDigits?: number;\n };\n\n if (resolved.fractionDigits !== undefined) {\n return resolved.fractionDigits;\n }\n\n // Use minimum/maximum as fallback\n if (resolved.minimumFractionDigits !== undefined) {\n return resolved.minimumFractionDigits;\n }\n } catch {\n // Fall through to fallback table\n }\n\n // Use fallback table\n return FRACTION_DIGITS[currency] ?? 2;\n}\n\n/**\n * Format a money amount using Intl.NumberFormat.\n *\n * @param amount - Amount in minor units (e.g. cents)\n * @param currency - ISO 4217 currency code\n * @param locale - Locale string (default: \"en-US\")\n * @returns Formatted currency string\n */\nexport function formatMoney(\n amount: number,\n currency: string,\n locale: string = \"en-US\",\n): string {\n const fractionDigits = getCurrencyFractionDigits(currency);\n const divisor = Math.pow(10, fractionDigits);\n const majorAmount = amount / divisor;\n\n const formatter = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency,\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits,\n });\n\n return formatter.format(majorAmount);\n}\n","/**\n * Largest-remainder method for allocating money amounts by ratios.\n * Ensures that parts sum exactly to the original amount with no penny lost.\n *\n * @param totalAmount - Total amount in minor units to allocate\n * @param ratios - Array of ratios for allocation (must sum > 0)\n * @returns Array of allocated amounts that sum to totalAmount\n * @throws RangeError if ratios don't sum to positive value\n */\nexport function allocate(\n totalAmount: number,\n ratios: readonly number[],\n): number[] {\n if (ratios.length === 0) {\n throw new RangeError(\"Cannot allocate with empty ratios\");\n }\n\n const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0);\n if (totalRatio <= 0) {\n throw new RangeError(\"Ratios must sum to a positive value\");\n }\n\n // Calculate initial floored allocations and remainders\n const allocations: number[] = [];\n const remainders: Array<{ index: number; remainder: number }> = [];\n let distributed = 0;\n\n for (let i = 0; i < ratios.length; i++) {\n const ratio = ratios[i];\n if (ratio === undefined) continue;\n\n const exact = (totalAmount * ratio) / totalRatio;\n const floored = Math.floor(exact);\n allocations.push(floored);\n remainders.push({ index: i, remainder: exact - floored });\n distributed += floored;\n }\n\n // Distribute remaining units to items with largest remainders\n const remainder = totalAmount - distributed;\n\n // Sort by remainder (descending) and give extra to items with largest remainders\n // Earlier items get priority when remainders are equal\n const sortedByRemainder = remainders\n .map((r, index) => ({ ...r, originalIndex: index }))\n .sort((a, b) => {\n if (b.remainder !== a.remainder) {\n return b.remainder - a.remainder; // Larger remainder first\n }\n return a.originalIndex - b.originalIndex; // Earlier index first for ties\n });\n\n for (let i = 0; i < remainder && i < sortedByRemainder.length; i++) {\n const item = sortedByRemainder[i];\n if (item) {\n const idx = item.originalIndex;\n allocations[idx] = (allocations[idx] ?? 0) + 1;\n }\n }\n\n return allocations;\n}\n","/**\n * Error thrown when operations are attempted on Money objects with different currencies.\n */\nexport class CurrencyMismatch extends Error {\n constructor() {\n super(\"Currency mismatch: operations require matching currencies\");\n this.name = \"CurrencyMismatch\";\n }\n}\n","import { formatMoney, getCurrencyFractionDigits } from \"./format.js\";\nimport { allocate } from \"./allocate.js\";\nimport { CurrencyMismatch } from \"./errors.js\";\n\n/**\n * Rounding modes for multiply operations.\n */\nexport type Rounding = \"half-up\" | \"half-even\" | \"down\" | \"up\";\n\n/**\n * Money value object - immutable representation of an amount in a specific currency.\n * Uses integer minor units (e.g. cents) to avoid floating-point rounding errors.\n */\nexport interface Money {\n /** Amount in minor units (e.g. cents for USD) - must be a safe integer */\n readonly amount: number;\n /** ISO 4217 currency code */\n readonly currency: string;\n\n /**\n * Add another Money value (same currency only).\n * @throws CurrencyMismatch if currencies differ\n */\n add(other: Money): Money;\n\n /**\n * Subtract another Money value (same currency only).\n * @throws CurrencyMismatch if currencies differ\n */\n subtract(other: Money): Money;\n\n /**\n * Multiply by a factor with optional rounding mode.\n * @param factor - Multiplication factor (can be fractional, e.g. 1.0825 for 8.25% tax)\n * @param rounding - Rounding mode (default: \"half-even\")\n */\n multiply(factor: number, rounding?: Rounding): Money;\n\n /**\n * Allocate amount by integer ratios using largest-remainder method.\n * Parts always sum exactly to the original amount.\n * @param ratios - Array of ratios (must sum > 0)\n * @throws RangeError if ratios don't sum to positive value\n */\n allocate(ratios: readonly number[]): Money[];\n\n /**\n * Compare with another Money value (same currency only).\n * @returns -1 if less, 0 if equal, 1 if greater\n * @throws CurrencyMismatch if currencies differ\n */\n compare(other: Money): -1 | 0 | 1;\n\n /**\n * Check equality with another Money value (same currency only).\n * @throws CurrencyMismatch if currencies differ\n */\n equals(other: Money): boolean;\n\n /** Check if amount is zero */\n isZero(): boolean;\n /** Check if amount is negative */\n isNegative(): boolean;\n /** Check if amount is positive */\n isPositive(): boolean;\n\n /**\n * Format using Intl.NumberFormat.\n * @param locale - Locale string (default: \"en-US\")\n */\n format(locale?: string): string;\n\n /** Serialize to plain object */\n toJSON(): { amount: number; currency: string };\n}\n\n/**\n * Create a Money implementation class.\n */\nclass MoneyImpl implements Money {\n constructor(\n public readonly amount: number,\n public readonly currency: string,\n ) {\n // Validate safe integer\n if (!Number.isSafeInteger(amount)) {\n throw new RangeError(\n `Amount must be a safe integer, got: ${String(amount)}`,\n );\n }\n }\n\n add(other: Money): Money {\n this.ensureSameCurrency(other);\n return new MoneyImpl(this.amount + other.amount, this.currency);\n }\n\n subtract(other: Money): Money {\n this.ensureSameCurrency(other);\n return new MoneyImpl(this.amount - other.amount, this.currency);\n }\n\n multiply(factor: number, rounding: Rounding = \"half-even\"): Money {\n const exact = this.amount * factor;\n const rounded = this.applyRounding(exact, rounding);\n\n // Check if result exceeds safe integer range before capping\n if (rounded > Number.MAX_SAFE_INTEGER) {\n return new MoneyImpl(Number.MAX_SAFE_INTEGER, this.currency);\n }\n if (rounded < Number.MIN_SAFE_INTEGER) {\n return new MoneyImpl(Number.MIN_SAFE_INTEGER, this.currency);\n }\n\n return new MoneyImpl(rounded, this.currency);\n }\n\n allocate(ratios: readonly number[]): Money[] {\n const allocations = allocate(this.amount, ratios);\n return allocations.map((amount) => new MoneyImpl(amount, this.currency));\n }\n\n compare(other: Money): -1 | 0 | 1 {\n this.ensureSameCurrency(other);\n if (this.amount < other.amount) return -1;\n if (this.amount > other.amount) return 1;\n return 0;\n }\n\n equals(other: Money): boolean {\n return this.compare(other) === 0;\n }\n\n isZero(): boolean {\n return this.amount === 0;\n }\n\n isNegative(): boolean {\n return this.amount < 0;\n }\n\n isPositive(): boolean {\n return this.amount > 0;\n }\n\n format(locale: string = \"en-US\"): string {\n return formatMoney(this.amount, this.currency, locale);\n }\n\n toJSON(): { amount: number; currency: string } {\n return { amount: this.amount, currency: this.currency };\n }\n\n private ensureSameCurrency(other: Money): void {\n if (this.currency !== other.currency) {\n throw new CurrencyMismatch();\n }\n }\n\n private applyRounding(value: number, mode: Rounding): number {\n if (Number.isSafeInteger(value)) {\n return value;\n }\n\n switch (mode) {\n case \"half-up\": {\n // Round to nearest, with ties going up\n const fractional = value - Math.floor(value);\n if (fractional === 0.5) {\n return Math.ceil(value);\n }\n return Math.round(value);\n }\n case \"half-even\": {\n // Banker's rounding: round to nearest even integer\n const rounded = Math.round(value);\n const isHalf = Math.abs(value - rounded) === 0.5;\n if (isHalf) {\n // Round to nearest even\n return Math.round(value / 2) * 2;\n }\n return rounded;\n }\n case \"down\":\n return Math.floor(value);\n case \"up\":\n return Math.ceil(value);\n default:\n return Math.round(value);\n }\n }\n}\n\n/**\n * Factory function to create Money objects.\n *\n * @param amount - Amount in minor units (e.g. cents for USD)\n * @param currency - ISO 4217 currency code\n * @throws RangeError if amount is not a safe integer\n */\nexport function money(amount: number, currency: string): Money {\n return new MoneyImpl(amount, currency);\n}\n\n/**\n * Create Money from a decimal string representation.\n * Parses a string like \"19.99\" to 1999 cents for USD.\n * Never uses float parsing to avoid reintroducing float errors.\n *\n * @param value - Decimal string (e.g. \"19.99\")\n * @param currency - ISO 4217 currency code\n * @throws RangeError if value has more fraction digits than currency allows\n */\nexport function fromDecimal(value: string, currency: string): Money {\n // Get fraction digits for currency (FIX: use the proper function)\n const fractionDigits = getCurrencyFractionDigits(currency);\n\n // Parse the string\n const parts = value.split(\".\");\n if (parts.length > 2) {\n throw new RangeError(`Invalid decimal format: ${value}`);\n }\n\n const integerPart = parts[0] || \"0\";\n const fractionalPart = parts[1] || \"\";\n\n // Handle negative numbers\n const isNegative = integerPart.startsWith(\"-\");\n const absIntegerPart = isNegative ? integerPart.slice(1) : integerPart;\n\n // Handle special case: \"-0\" or \"-0.\" should be 0\n if (\n absIntegerPart === \"0\" &&\n (fractionalPart === \"\" || fractionalPart === \"0\")\n ) {\n return money(0, currency);\n }\n\n // Validate fraction digits\n if (fractionalPart.length > fractionDigits) {\n // Check if it can be cleanly rounded\n const hasExtraDigits = fractionalPart\n .slice(fractionDigits)\n .split(\"\")\n .some((d) => d !== \"0\");\n if (hasExtraDigits) {\n throw new RangeError(\n `Too many fraction digits for ${currency}: ${value} (max ${String(fractionDigits)})`,\n );\n }\n }\n\n // Convert to integer minor units\n const majorUnits = parseInt(absIntegerPart, 10);\n let minorUnits = 0;\n\n if (fractionalPart.length > 0) {\n const padded = fractionalPart.padEnd(fractionDigits, \"0\");\n minorUnits = parseInt(padded, 10);\n }\n\n const totalAmount = majorUnits * Math.pow(10, fractionDigits) + minorUnits;\n return money(isNegative ? -totalAmount : totalAmount, currency);\n}\n\n// Re-export CurrencyMismatch for convenience\nexport { CurrencyMismatch };\n"],"mappings":"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","names":[]}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Error thrown when operations are attempted on Money objects with different currencies.
3
+ */
4
+ declare class CurrencyMismatch extends Error {
5
+ constructor();
6
+ }
7
+
8
+ /**
9
+ * Rounding modes for multiply operations.
10
+ */
11
+ type Rounding = "half-up" | "half-even" | "down" | "up";
12
+ /**
13
+ * Money value object - immutable representation of an amount in a specific currency.
14
+ * Uses integer minor units (e.g. cents) to avoid floating-point rounding errors.
15
+ */
16
+ interface Money {
17
+ /** Amount in minor units (e.g. cents for USD) - must be a safe integer */
18
+ readonly amount: number;
19
+ /** ISO 4217 currency code */
20
+ readonly currency: string;
21
+ /**
22
+ * Add another Money value (same currency only).
23
+ * @throws CurrencyMismatch if currencies differ
24
+ */
25
+ add(other: Money): Money;
26
+ /**
27
+ * Subtract another Money value (same currency only).
28
+ * @throws CurrencyMismatch if currencies differ
29
+ */
30
+ subtract(other: Money): Money;
31
+ /**
32
+ * Multiply by a factor with optional rounding mode.
33
+ * @param factor - Multiplication factor (can be fractional, e.g. 1.0825 for 8.25% tax)
34
+ * @param rounding - Rounding mode (default: "half-even")
35
+ */
36
+ multiply(factor: number, rounding?: Rounding): Money;
37
+ /**
38
+ * Allocate amount by integer ratios using largest-remainder method.
39
+ * Parts always sum exactly to the original amount.
40
+ * @param ratios - Array of ratios (must sum > 0)
41
+ * @throws RangeError if ratios don't sum to positive value
42
+ */
43
+ allocate(ratios: readonly number[]): Money[];
44
+ /**
45
+ * Compare with another Money value (same currency only).
46
+ * @returns -1 if less, 0 if equal, 1 if greater
47
+ * @throws CurrencyMismatch if currencies differ
48
+ */
49
+ compare(other: Money): -1 | 0 | 1;
50
+ /**
51
+ * Check equality with another Money value (same currency only).
52
+ * @throws CurrencyMismatch if currencies differ
53
+ */
54
+ equals(other: Money): boolean;
55
+ /** Check if amount is zero */
56
+ isZero(): boolean;
57
+ /** Check if amount is negative */
58
+ isNegative(): boolean;
59
+ /** Check if amount is positive */
60
+ isPositive(): boolean;
61
+ /**
62
+ * Format using Intl.NumberFormat.
63
+ * @param locale - Locale string (default: "en-US")
64
+ */
65
+ format(locale?: string): string;
66
+ /** Serialize to plain object */
67
+ toJSON(): {
68
+ amount: number;
69
+ currency: string;
70
+ };
71
+ }
72
+ /**
73
+ * Factory function to create Money objects.
74
+ *
75
+ * @param amount - Amount in minor units (e.g. cents for USD)
76
+ * @param currency - ISO 4217 currency code
77
+ * @throws RangeError if amount is not a safe integer
78
+ */
79
+ declare function money(amount: number, currency: string): Money;
80
+ /**
81
+ * Create Money from a decimal string representation.
82
+ * Parses a string like "19.99" to 1999 cents for USD.
83
+ * Never uses float parsing to avoid reintroducing float errors.
84
+ *
85
+ * @param value - Decimal string (e.g. "19.99")
86
+ * @param currency - ISO 4217 currency code
87
+ * @throws RangeError if value has more fraction digits than currency allows
88
+ */
89
+ declare function fromDecimal(value: string, currency: string): Money;
90
+
91
+ export { CurrencyMismatch, type Money, type Rounding, money as default, fromDecimal, money };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Error thrown when operations are attempted on Money objects with different currencies.
3
+ */
4
+ declare class CurrencyMismatch extends Error {
5
+ constructor();
6
+ }
7
+
8
+ /**
9
+ * Rounding modes for multiply operations.
10
+ */
11
+ type Rounding = "half-up" | "half-even" | "down" | "up";
12
+ /**
13
+ * Money value object - immutable representation of an amount in a specific currency.
14
+ * Uses integer minor units (e.g. cents) to avoid floating-point rounding errors.
15
+ */
16
+ interface Money {
17
+ /** Amount in minor units (e.g. cents for USD) - must be a safe integer */
18
+ readonly amount: number;
19
+ /** ISO 4217 currency code */
20
+ readonly currency: string;
21
+ /**
22
+ * Add another Money value (same currency only).
23
+ * @throws CurrencyMismatch if currencies differ
24
+ */
25
+ add(other: Money): Money;
26
+ /**
27
+ * Subtract another Money value (same currency only).
28
+ * @throws CurrencyMismatch if currencies differ
29
+ */
30
+ subtract(other: Money): Money;
31
+ /**
32
+ * Multiply by a factor with optional rounding mode.
33
+ * @param factor - Multiplication factor (can be fractional, e.g. 1.0825 for 8.25% tax)
34
+ * @param rounding - Rounding mode (default: "half-even")
35
+ */
36
+ multiply(factor: number, rounding?: Rounding): Money;
37
+ /**
38
+ * Allocate amount by integer ratios using largest-remainder method.
39
+ * Parts always sum exactly to the original amount.
40
+ * @param ratios - Array of ratios (must sum > 0)
41
+ * @throws RangeError if ratios don't sum to positive value
42
+ */
43
+ allocate(ratios: readonly number[]): Money[];
44
+ /**
45
+ * Compare with another Money value (same currency only).
46
+ * @returns -1 if less, 0 if equal, 1 if greater
47
+ * @throws CurrencyMismatch if currencies differ
48
+ */
49
+ compare(other: Money): -1 | 0 | 1;
50
+ /**
51
+ * Check equality with another Money value (same currency only).
52
+ * @throws CurrencyMismatch if currencies differ
53
+ */
54
+ equals(other: Money): boolean;
55
+ /** Check if amount is zero */
56
+ isZero(): boolean;
57
+ /** Check if amount is negative */
58
+ isNegative(): boolean;
59
+ /** Check if amount is positive */
60
+ isPositive(): boolean;
61
+ /**
62
+ * Format using Intl.NumberFormat.
63
+ * @param locale - Locale string (default: "en-US")
64
+ */
65
+ format(locale?: string): string;
66
+ /** Serialize to plain object */
67
+ toJSON(): {
68
+ amount: number;
69
+ currency: string;
70
+ };
71
+ }
72
+ /**
73
+ * Factory function to create Money objects.
74
+ *
75
+ * @param amount - Amount in minor units (e.g. cents for USD)
76
+ * @param currency - ISO 4217 currency code
77
+ * @throws RangeError if amount is not a safe integer
78
+ */
79
+ declare function money(amount: number, currency: string): Money;
80
+ /**
81
+ * Create Money from a decimal string representation.
82
+ * Parses a string like "19.99" to 1999 cents for USD.
83
+ * Never uses float parsing to avoid reintroducing float errors.
84
+ *
85
+ * @param value - Decimal string (e.g. "19.99")
86
+ * @param currency - ISO 4217 currency code
87
+ * @throws RangeError if value has more fraction digits than currency allows
88
+ */
89
+ declare function fromDecimal(value: string, currency: string): Money;
90
+
91
+ export { CurrencyMismatch, type Money, type Rounding, money as default, fromDecimal, money };
package/dist/index.js ADDED
@@ -0,0 +1,222 @@
1
+ // src/format.ts
2
+ var FRACTION_DIGITS = {
3
+ JPY: 0,
4
+ USD: 2,
5
+ EUR: 2,
6
+ GBP: 2,
7
+ CAD: 2,
8
+ AUD: 2,
9
+ CHF: 2,
10
+ CNY: 2,
11
+ INR: 2
12
+ };
13
+ function getCurrencyFractionDigits(currency) {
14
+ try {
15
+ const formatter = new Intl.NumberFormat("en-US", {
16
+ style: "currency",
17
+ currency
18
+ });
19
+ const resolved = formatter.resolvedOptions();
20
+ if (resolved.fractionDigits !== void 0) {
21
+ return resolved.fractionDigits;
22
+ }
23
+ if (resolved.minimumFractionDigits !== void 0) {
24
+ return resolved.minimumFractionDigits;
25
+ }
26
+ } catch {
27
+ }
28
+ return FRACTION_DIGITS[currency] ?? 2;
29
+ }
30
+ function formatMoney(amount, currency, locale = "en-US") {
31
+ const fractionDigits = getCurrencyFractionDigits(currency);
32
+ const divisor = Math.pow(10, fractionDigits);
33
+ const majorAmount = amount / divisor;
34
+ const formatter = new Intl.NumberFormat(locale, {
35
+ style: "currency",
36
+ currency,
37
+ minimumFractionDigits: fractionDigits,
38
+ maximumFractionDigits: fractionDigits
39
+ });
40
+ return formatter.format(majorAmount);
41
+ }
42
+
43
+ // src/allocate.ts
44
+ function allocate(totalAmount, ratios) {
45
+ if (ratios.length === 0) {
46
+ throw new RangeError("Cannot allocate with empty ratios");
47
+ }
48
+ const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0);
49
+ if (totalRatio <= 0) {
50
+ throw new RangeError("Ratios must sum to a positive value");
51
+ }
52
+ const allocations = [];
53
+ const remainders = [];
54
+ let distributed = 0;
55
+ for (let i = 0; i < ratios.length; i++) {
56
+ const ratio = ratios[i];
57
+ if (ratio === void 0) continue;
58
+ const exact = totalAmount * ratio / totalRatio;
59
+ const floored = Math.floor(exact);
60
+ allocations.push(floored);
61
+ remainders.push({ index: i, remainder: exact - floored });
62
+ distributed += floored;
63
+ }
64
+ const remainder = totalAmount - distributed;
65
+ const sortedByRemainder = remainders.map((r, index) => ({ ...r, originalIndex: index })).sort((a, b) => {
66
+ if (b.remainder !== a.remainder) {
67
+ return b.remainder - a.remainder;
68
+ }
69
+ return a.originalIndex - b.originalIndex;
70
+ });
71
+ for (let i = 0; i < remainder && i < sortedByRemainder.length; i++) {
72
+ const item = sortedByRemainder[i];
73
+ if (item) {
74
+ const idx = item.originalIndex;
75
+ allocations[idx] = (allocations[idx] ?? 0) + 1;
76
+ }
77
+ }
78
+ return allocations;
79
+ }
80
+
81
+ // src/errors.ts
82
+ var CurrencyMismatch = class extends Error {
83
+ constructor() {
84
+ super("Currency mismatch: operations require matching currencies");
85
+ this.name = "CurrencyMismatch";
86
+ }
87
+ };
88
+
89
+ // src/money.ts
90
+ var MoneyImpl = class _MoneyImpl {
91
+ constructor(amount, currency) {
92
+ this.amount = amount;
93
+ this.currency = currency;
94
+ if (!Number.isSafeInteger(amount)) {
95
+ throw new RangeError(
96
+ `Amount must be a safe integer, got: ${String(amount)}`
97
+ );
98
+ }
99
+ }
100
+ amount;
101
+ currency;
102
+ add(other) {
103
+ this.ensureSameCurrency(other);
104
+ return new _MoneyImpl(this.amount + other.amount, this.currency);
105
+ }
106
+ subtract(other) {
107
+ this.ensureSameCurrency(other);
108
+ return new _MoneyImpl(this.amount - other.amount, this.currency);
109
+ }
110
+ multiply(factor, rounding = "half-even") {
111
+ const exact = this.amount * factor;
112
+ const rounded = this.applyRounding(exact, rounding);
113
+ if (rounded > Number.MAX_SAFE_INTEGER) {
114
+ return new _MoneyImpl(Number.MAX_SAFE_INTEGER, this.currency);
115
+ }
116
+ if (rounded < Number.MIN_SAFE_INTEGER) {
117
+ return new _MoneyImpl(Number.MIN_SAFE_INTEGER, this.currency);
118
+ }
119
+ return new _MoneyImpl(rounded, this.currency);
120
+ }
121
+ allocate(ratios) {
122
+ const allocations = allocate(this.amount, ratios);
123
+ return allocations.map((amount) => new _MoneyImpl(amount, this.currency));
124
+ }
125
+ compare(other) {
126
+ this.ensureSameCurrency(other);
127
+ if (this.amount < other.amount) return -1;
128
+ if (this.amount > other.amount) return 1;
129
+ return 0;
130
+ }
131
+ equals(other) {
132
+ return this.compare(other) === 0;
133
+ }
134
+ isZero() {
135
+ return this.amount === 0;
136
+ }
137
+ isNegative() {
138
+ return this.amount < 0;
139
+ }
140
+ isPositive() {
141
+ return this.amount > 0;
142
+ }
143
+ format(locale = "en-US") {
144
+ return formatMoney(this.amount, this.currency, locale);
145
+ }
146
+ toJSON() {
147
+ return { amount: this.amount, currency: this.currency };
148
+ }
149
+ ensureSameCurrency(other) {
150
+ if (this.currency !== other.currency) {
151
+ throw new CurrencyMismatch();
152
+ }
153
+ }
154
+ applyRounding(value, mode) {
155
+ if (Number.isSafeInteger(value)) {
156
+ return value;
157
+ }
158
+ switch (mode) {
159
+ case "half-up": {
160
+ const fractional = value - Math.floor(value);
161
+ if (fractional === 0.5) {
162
+ return Math.ceil(value);
163
+ }
164
+ return Math.round(value);
165
+ }
166
+ case "half-even": {
167
+ const rounded = Math.round(value);
168
+ const isHalf = Math.abs(value - rounded) === 0.5;
169
+ if (isHalf) {
170
+ return Math.round(value / 2) * 2;
171
+ }
172
+ return rounded;
173
+ }
174
+ case "down":
175
+ return Math.floor(value);
176
+ case "up":
177
+ return Math.ceil(value);
178
+ default:
179
+ return Math.round(value);
180
+ }
181
+ }
182
+ };
183
+ function money(amount, currency) {
184
+ return new MoneyImpl(amount, currency);
185
+ }
186
+ function fromDecimal(value, currency) {
187
+ const fractionDigits = getCurrencyFractionDigits(currency);
188
+ const parts = value.split(".");
189
+ if (parts.length > 2) {
190
+ throw new RangeError(`Invalid decimal format: ${value}`);
191
+ }
192
+ const integerPart = parts[0] || "0";
193
+ const fractionalPart = parts[1] || "";
194
+ const isNegative = integerPart.startsWith("-");
195
+ const absIntegerPart = isNegative ? integerPart.slice(1) : integerPart;
196
+ if (absIntegerPart === "0" && (fractionalPart === "" || fractionalPart === "0")) {
197
+ return money(0, currency);
198
+ }
199
+ if (fractionalPart.length > fractionDigits) {
200
+ const hasExtraDigits = fractionalPart.slice(fractionDigits).split("").some((d) => d !== "0");
201
+ if (hasExtraDigits) {
202
+ throw new RangeError(
203
+ `Too many fraction digits for ${currency}: ${value} (max ${String(fractionDigits)})`
204
+ );
205
+ }
206
+ }
207
+ const majorUnits = parseInt(absIntegerPart, 10);
208
+ let minorUnits = 0;
209
+ if (fractionalPart.length > 0) {
210
+ const padded = fractionalPart.padEnd(fractionDigits, "0");
211
+ minorUnits = parseInt(padded, 10);
212
+ }
213
+ const totalAmount = majorUnits * Math.pow(10, fractionDigits) + minorUnits;
214
+ return money(isNegative ? -totalAmount : totalAmount, currency);
215
+ }
216
+ export {
217
+ CurrencyMismatch,
218
+ money as default,
219
+ fromDecimal,
220
+ money
221
+ };
222
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/format.ts","../src/allocate.ts","../src/errors.ts","../src/money.ts"],"sourcesContent":["/**\n * Currency fraction digits fallback table for common currencies.\n * Used when Intl.NumberFormat is not available or returns unexpected results.\n */\nconst FRACTION_DIGITS: Record<string, number> = {\n JPY: 0,\n USD: 2,\n EUR: 2,\n GBP: 2,\n CAD: 2,\n AUD: 2,\n CHF: 2,\n CNY: 2,\n INR: 2,\n};\n\n/**\n * Get the number of fraction digits for a currency code.\n * Uses Intl.NumberFormat if available, with fallback table.\n *\n * @param currency - ISO 4217 currency code\n * @returns Number of fraction digits (usually 0 or 2)\n */\nexport function getCurrencyFractionDigits(currency: string): number {\n try {\n // Try to get from Intl\n const formatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency,\n });\n\n const resolved =\n formatter.resolvedOptions() as Intl.ResolvedNumberFormatOptions & {\n fractionDigits?: number;\n minimumFractionDigits?: number;\n maximumFractionDigits?: number;\n };\n\n if (resolved.fractionDigits !== undefined) {\n return resolved.fractionDigits;\n }\n\n // Use minimum/maximum as fallback\n if (resolved.minimumFractionDigits !== undefined) {\n return resolved.minimumFractionDigits;\n }\n } catch {\n // Fall through to fallback table\n }\n\n // Use fallback table\n return FRACTION_DIGITS[currency] ?? 2;\n}\n\n/**\n * Format a money amount using Intl.NumberFormat.\n *\n * @param amount - Amount in minor units (e.g. cents)\n * @param currency - ISO 4217 currency code\n * @param locale - Locale string (default: \"en-US\")\n * @returns Formatted currency string\n */\nexport function formatMoney(\n amount: number,\n currency: string,\n locale: string = \"en-US\",\n): string {\n const fractionDigits = getCurrencyFractionDigits(currency);\n const divisor = Math.pow(10, fractionDigits);\n const majorAmount = amount / divisor;\n\n const formatter = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency,\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits,\n });\n\n return formatter.format(majorAmount);\n}\n","/**\n * Largest-remainder method for allocating money amounts by ratios.\n * Ensures that parts sum exactly to the original amount with no penny lost.\n *\n * @param totalAmount - Total amount in minor units to allocate\n * @param ratios - Array of ratios for allocation (must sum > 0)\n * @returns Array of allocated amounts that sum to totalAmount\n * @throws RangeError if ratios don't sum to positive value\n */\nexport function allocate(\n totalAmount: number,\n ratios: readonly number[],\n): number[] {\n if (ratios.length === 0) {\n throw new RangeError(\"Cannot allocate with empty ratios\");\n }\n\n const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0);\n if (totalRatio <= 0) {\n throw new RangeError(\"Ratios must sum to a positive value\");\n }\n\n // Calculate initial floored allocations and remainders\n const allocations: number[] = [];\n const remainders: Array<{ index: number; remainder: number }> = [];\n let distributed = 0;\n\n for (let i = 0; i < ratios.length; i++) {\n const ratio = ratios[i];\n if (ratio === undefined) continue;\n\n const exact = (totalAmount * ratio) / totalRatio;\n const floored = Math.floor(exact);\n allocations.push(floored);\n remainders.push({ index: i, remainder: exact - floored });\n distributed += floored;\n }\n\n // Distribute remaining units to items with largest remainders\n const remainder = totalAmount - distributed;\n\n // Sort by remainder (descending) and give extra to items with largest remainders\n // Earlier items get priority when remainders are equal\n const sortedByRemainder = remainders\n .map((r, index) => ({ ...r, originalIndex: index }))\n .sort((a, b) => {\n if (b.remainder !== a.remainder) {\n return b.remainder - a.remainder; // Larger remainder first\n }\n return a.originalIndex - b.originalIndex; // Earlier index first for ties\n });\n\n for (let i = 0; i < remainder && i < sortedByRemainder.length; i++) {\n const item = sortedByRemainder[i];\n if (item) {\n const idx = item.originalIndex;\n allocations[idx] = (allocations[idx] ?? 0) + 1;\n }\n }\n\n return allocations;\n}\n","/**\n * Error thrown when operations are attempted on Money objects with different currencies.\n */\nexport class CurrencyMismatch extends Error {\n constructor() {\n super(\"Currency mismatch: operations require matching currencies\");\n this.name = \"CurrencyMismatch\";\n }\n}\n","import { formatMoney, getCurrencyFractionDigits } from \"./format.js\";\nimport { allocate } from \"./allocate.js\";\nimport { CurrencyMismatch } from \"./errors.js\";\n\n/**\n * Rounding modes for multiply operations.\n */\nexport type Rounding = \"half-up\" | \"half-even\" | \"down\" | \"up\";\n\n/**\n * Money value object - immutable representation of an amount in a specific currency.\n * Uses integer minor units (e.g. cents) to avoid floating-point rounding errors.\n */\nexport interface Money {\n /** Amount in minor units (e.g. cents for USD) - must be a safe integer */\n readonly amount: number;\n /** ISO 4217 currency code */\n readonly currency: string;\n\n /**\n * Add another Money value (same currency only).\n * @throws CurrencyMismatch if currencies differ\n */\n add(other: Money): Money;\n\n /**\n * Subtract another Money value (same currency only).\n * @throws CurrencyMismatch if currencies differ\n */\n subtract(other: Money): Money;\n\n /**\n * Multiply by a factor with optional rounding mode.\n * @param factor - Multiplication factor (can be fractional, e.g. 1.0825 for 8.25% tax)\n * @param rounding - Rounding mode (default: \"half-even\")\n */\n multiply(factor: number, rounding?: Rounding): Money;\n\n /**\n * Allocate amount by integer ratios using largest-remainder method.\n * Parts always sum exactly to the original amount.\n * @param ratios - Array of ratios (must sum > 0)\n * @throws RangeError if ratios don't sum to positive value\n */\n allocate(ratios: readonly number[]): Money[];\n\n /**\n * Compare with another Money value (same currency only).\n * @returns -1 if less, 0 if equal, 1 if greater\n * @throws CurrencyMismatch if currencies differ\n */\n compare(other: Money): -1 | 0 | 1;\n\n /**\n * Check equality with another Money value (same currency only).\n * @throws CurrencyMismatch if currencies differ\n */\n equals(other: Money): boolean;\n\n /** Check if amount is zero */\n isZero(): boolean;\n /** Check if amount is negative */\n isNegative(): boolean;\n /** Check if amount is positive */\n isPositive(): boolean;\n\n /**\n * Format using Intl.NumberFormat.\n * @param locale - Locale string (default: \"en-US\")\n */\n format(locale?: string): string;\n\n /** Serialize to plain object */\n toJSON(): { amount: number; currency: string };\n}\n\n/**\n * Create a Money implementation class.\n */\nclass MoneyImpl implements Money {\n constructor(\n public readonly amount: number,\n public readonly currency: string,\n ) {\n // Validate safe integer\n if (!Number.isSafeInteger(amount)) {\n throw new RangeError(\n `Amount must be a safe integer, got: ${String(amount)}`,\n );\n }\n }\n\n add(other: Money): Money {\n this.ensureSameCurrency(other);\n return new MoneyImpl(this.amount + other.amount, this.currency);\n }\n\n subtract(other: Money): Money {\n this.ensureSameCurrency(other);\n return new MoneyImpl(this.amount - other.amount, this.currency);\n }\n\n multiply(factor: number, rounding: Rounding = \"half-even\"): Money {\n const exact = this.amount * factor;\n const rounded = this.applyRounding(exact, rounding);\n\n // Check if result exceeds safe integer range before capping\n if (rounded > Number.MAX_SAFE_INTEGER) {\n return new MoneyImpl(Number.MAX_SAFE_INTEGER, this.currency);\n }\n if (rounded < Number.MIN_SAFE_INTEGER) {\n return new MoneyImpl(Number.MIN_SAFE_INTEGER, this.currency);\n }\n\n return new MoneyImpl(rounded, this.currency);\n }\n\n allocate(ratios: readonly number[]): Money[] {\n const allocations = allocate(this.amount, ratios);\n return allocations.map((amount) => new MoneyImpl(amount, this.currency));\n }\n\n compare(other: Money): -1 | 0 | 1 {\n this.ensureSameCurrency(other);\n if (this.amount < other.amount) return -1;\n if (this.amount > other.amount) return 1;\n return 0;\n }\n\n equals(other: Money): boolean {\n return this.compare(other) === 0;\n }\n\n isZero(): boolean {\n return this.amount === 0;\n }\n\n isNegative(): boolean {\n return this.amount < 0;\n }\n\n isPositive(): boolean {\n return this.amount > 0;\n }\n\n format(locale: string = \"en-US\"): string {\n return formatMoney(this.amount, this.currency, locale);\n }\n\n toJSON(): { amount: number; currency: string } {\n return { amount: this.amount, currency: this.currency };\n }\n\n private ensureSameCurrency(other: Money): void {\n if (this.currency !== other.currency) {\n throw new CurrencyMismatch();\n }\n }\n\n private applyRounding(value: number, mode: Rounding): number {\n if (Number.isSafeInteger(value)) {\n return value;\n }\n\n switch (mode) {\n case \"half-up\": {\n // Round to nearest, with ties going up\n const fractional = value - Math.floor(value);\n if (fractional === 0.5) {\n return Math.ceil(value);\n }\n return Math.round(value);\n }\n case \"half-even\": {\n // Banker's rounding: round to nearest even integer\n const rounded = Math.round(value);\n const isHalf = Math.abs(value - rounded) === 0.5;\n if (isHalf) {\n // Round to nearest even\n return Math.round(value / 2) * 2;\n }\n return rounded;\n }\n case \"down\":\n return Math.floor(value);\n case \"up\":\n return Math.ceil(value);\n default:\n return Math.round(value);\n }\n }\n}\n\n/**\n * Factory function to create Money objects.\n *\n * @param amount - Amount in minor units (e.g. cents for USD)\n * @param currency - ISO 4217 currency code\n * @throws RangeError if amount is not a safe integer\n */\nexport function money(amount: number, currency: string): Money {\n return new MoneyImpl(amount, currency);\n}\n\n/**\n * Create Money from a decimal string representation.\n * Parses a string like \"19.99\" to 1999 cents for USD.\n * Never uses float parsing to avoid reintroducing float errors.\n *\n * @param value - Decimal string (e.g. \"19.99\")\n * @param currency - ISO 4217 currency code\n * @throws RangeError if value has more fraction digits than currency allows\n */\nexport function fromDecimal(value: string, currency: string): Money {\n // Get fraction digits for currency (FIX: use the proper function)\n const fractionDigits = getCurrencyFractionDigits(currency);\n\n // Parse the string\n const parts = value.split(\".\");\n if (parts.length > 2) {\n throw new RangeError(`Invalid decimal format: ${value}`);\n }\n\n const integerPart = parts[0] || \"0\";\n const fractionalPart = parts[1] || \"\";\n\n // Handle negative numbers\n const isNegative = integerPart.startsWith(\"-\");\n const absIntegerPart = isNegative ? integerPart.slice(1) : integerPart;\n\n // Handle special case: \"-0\" or \"-0.\" should be 0\n if (\n absIntegerPart === \"0\" &&\n (fractionalPart === \"\" || fractionalPart === \"0\")\n ) {\n return money(0, currency);\n }\n\n // Validate fraction digits\n if (fractionalPart.length > fractionDigits) {\n // Check if it can be cleanly rounded\n const hasExtraDigits = fractionalPart\n .slice(fractionDigits)\n .split(\"\")\n .some((d) => d !== \"0\");\n if (hasExtraDigits) {\n throw new RangeError(\n `Too many fraction digits for ${currency}: ${value} (max ${String(fractionDigits)})`,\n );\n }\n }\n\n // Convert to integer minor units\n const majorUnits = parseInt(absIntegerPart, 10);\n let minorUnits = 0;\n\n if (fractionalPart.length > 0) {\n const padded = fractionalPart.padEnd(fractionDigits, \"0\");\n minorUnits = parseInt(padded, 10);\n }\n\n const totalAmount = majorUnits * Math.pow(10, fractionDigits) + minorUnits;\n return money(isNegative ? -totalAmount : totalAmount, currency);\n}\n\n// Re-export CurrencyMismatch for convenience\nexport { CurrencyMismatch };\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@azghr/specie",
3
+ "version": "0.1.0",
4
+ "description": "Tiny integer-cents money type — safe arithmetic, currency-aware rounding, Intl formatting. No bignum, no dependencies.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "typecheck": "tsc --noEmit",
32
+ "lint": "eslint src test examples",
33
+ "demo": "tsx examples/demo.ts",
34
+ "check": "npm run typecheck && npm run lint && npm run test && npm run build",
35
+ "prepublishOnly": "npm run check"
36
+ },
37
+ "keywords": [
38
+ "money",
39
+ "currency",
40
+ "arithmetic",
41
+ "formatting",
42
+ "intl",
43
+ "cents",
44
+ "financial",
45
+ "decimal",
46
+ "rounding",
47
+ "allocation"
48
+ ],
49
+ "devDependencies": {
50
+ "@eslint/js": "^9.18.0",
51
+ "eslint": "^9.18.0",
52
+ "tsx": "^4.19.2",
53
+ "typescript": "^5.7.3",
54
+ "typescript-eslint": "^8.19.1",
55
+ "vitest": "^2.1.8",
56
+ "tsup": "^8.3.5"
57
+ }
58
+ }