soff-money 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/README.md +75 -0
- package/dist/core/index.cjs +244 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +181 -0
- package/dist/core/index.d.ts +181 -0
- package/dist/core/index.js +242 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.cjs +309 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +301 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/ar.cjs +16 -0
- package/dist/locales/ar.cjs.map +1 -0
- package/dist/locales/ar.d.cts +9 -0
- package/dist/locales/ar.d.ts +9 -0
- package/dist/locales/ar.js +14 -0
- package/dist/locales/ar.js.map +1 -0
- package/dist/locales/br.cjs +16 -0
- package/dist/locales/br.cjs.map +1 -0
- package/dist/locales/br.d.cts +9 -0
- package/dist/locales/br.d.ts +9 -0
- package/dist/locales/br.js +14 -0
- package/dist/locales/br.js.map +1 -0
- package/dist/locales/co.cjs +21 -0
- package/dist/locales/co.cjs.map +1 -0
- package/dist/locales/co.d.cts +14 -0
- package/dist/locales/co.d.ts +14 -0
- package/dist/locales/co.js +18 -0
- package/dist/locales/co.js.map +1 -0
- package/dist/locales/mx.cjs +16 -0
- package/dist/locales/mx.cjs.map +1 -0
- package/dist/locales/mx.d.cts +9 -0
- package/dist/locales/mx.d.ts +9 -0
- package/dist/locales/mx.js +14 -0
- package/dist/locales/mx.js.map +1 -0
- package/dist/locales/us.cjs +16 -0
- package/dist/locales/us.cjs.map +1 -0
- package/dist/locales/us.d.cts +9 -0
- package/dist/locales/us.d.ts +9 -0
- package/dist/locales/us.js +14 -0
- package/dist/locales/us.js.map +1 -0
- package/package.json +135 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# soff-money 💸
|
|
2
|
+
|
|
3
|
+
Safe money handling for JavaScript with integer-based arithmetic and LATAM locale formatting.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
In JavaScript, `0.1 + 0.2 === 0.30000000000000004`. This is fatal for e-commerce or financial applications. Additionally, formatting currencies in Latin America is painful - does the symbol go before or after? Dots or commas for thousands?
|
|
8
|
+
|
|
9
|
+
## The Solution
|
|
10
|
+
|
|
11
|
+
A library that handles money using integers (Safe Money pattern) and formats according to the country's locale.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🔢 **Integer-based arithmetic** - No floating point errors
|
|
16
|
+
- 🌎 **LATAM-first locales** - CO, MX, AR, BR, US
|
|
17
|
+
- 🌳 **Tree-shakeable** - Import only what you need
|
|
18
|
+
- 📦 **Tiny bundle** - Core is < 2KB gzipped
|
|
19
|
+
- 💯 **TypeScript** - Full type safety
|
|
20
|
+
- âš¡ **Zero dependencies** - Pure JavaScript
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install soff-money
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Money } from 'soff-money';
|
|
32
|
+
import { COP } from 'soff-money/locales/co';
|
|
33
|
+
|
|
34
|
+
// Create money from cents (safe)
|
|
35
|
+
const price = Money.fromCents(10000, COP); // $100.00 COP
|
|
36
|
+
|
|
37
|
+
// Arithmetic operations
|
|
38
|
+
const total = price.add(Money.fromCents(5000, COP)); // $150.00
|
|
39
|
+
const discounted = total.multiply(0.9); // $135.00
|
|
40
|
+
|
|
41
|
+
// Format for display
|
|
42
|
+
console.log(total.format()); // "$ 150,00" or "$150.00" depending on locale
|
|
43
|
+
|
|
44
|
+
// Safe distribution (no lost cents!)
|
|
45
|
+
const [part1, part2, part3] = Money.fromCents(10000, COP).distribute(3);
|
|
46
|
+
// 33.34 + 33.33 + 33.33 = 100.00 ✓
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Killer Feature: Fair Distribution
|
|
50
|
+
|
|
51
|
+
When splitting money, you never lose cents:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const bill = Money.fromCents(10000, COP); // $100.00
|
|
55
|
+
const [alice, bob, charlie] = bill.distribute(3);
|
|
56
|
+
|
|
57
|
+
// alice: $33.34
|
|
58
|
+
// bob: $33.33
|
|
59
|
+
// charlie: $33.33
|
|
60
|
+
// Total: $100.00 ✓ (not $99.99!)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Available Locales
|
|
64
|
+
|
|
65
|
+
| Locale | Currency | Symbol | Format |
|
|
66
|
+
| ------ | -------- | ------ | ----------- |
|
|
67
|
+
| `co` | COP | $ | $ 1.000,00 |
|
|
68
|
+
| `mx` | MXN | $ | $1,000.00 |
|
|
69
|
+
| `ar` | ARS | $ | $ 1.000,00 |
|
|
70
|
+
| `br` | BRL | R$ | R$ 1.000,00 |
|
|
71
|
+
| `us` | USD | $ | $1,000.00 |
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/money.ts
|
|
4
|
+
var Money = class _Money {
|
|
5
|
+
constructor(cents, currency) {
|
|
6
|
+
if (!Number.isInteger(cents)) {
|
|
7
|
+
throw new Error("Money must be created with an integer number of cents");
|
|
8
|
+
}
|
|
9
|
+
this.cents = cents;
|
|
10
|
+
this.currency = currency;
|
|
11
|
+
Object.freeze(this);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create money from cents (smallest unit)
|
|
15
|
+
*/
|
|
16
|
+
static fromCents(cents, currency) {
|
|
17
|
+
return new _Money(Math.round(cents), currency);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create money from a decimal amount
|
|
21
|
+
* @example Money.fromDecimal(100.50, USD) creates $100.50
|
|
22
|
+
*/
|
|
23
|
+
static fromDecimal(amount, currency) {
|
|
24
|
+
const multiplier = Math.pow(10, currency.decimals);
|
|
25
|
+
const cents = Math.round(amount * multiplier);
|
|
26
|
+
return new _Money(cents, currency);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create zero money
|
|
30
|
+
*/
|
|
31
|
+
static zero(currency) {
|
|
32
|
+
return new _Money(0, currency);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Add another money value (must be same currency)
|
|
36
|
+
*/
|
|
37
|
+
add(other) {
|
|
38
|
+
this.assertSameCurrency(other);
|
|
39
|
+
return new _Money(this.cents + other.cents, this.currency);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Subtract another money value (must be same currency)
|
|
43
|
+
*/
|
|
44
|
+
subtract(other) {
|
|
45
|
+
this.assertSameCurrency(other);
|
|
46
|
+
return new _Money(this.cents - other.cents, this.currency);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Multiply by a factor (rounds to nearest cent)
|
|
50
|
+
*/
|
|
51
|
+
multiply(factor) {
|
|
52
|
+
return new _Money(Math.round(this.cents * factor), this.currency);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Divide by a divisor (rounds to nearest cent)
|
|
56
|
+
*/
|
|
57
|
+
divide(divisor) {
|
|
58
|
+
if (divisor === 0) {
|
|
59
|
+
throw new Error("Cannot divide by zero");
|
|
60
|
+
}
|
|
61
|
+
return new _Money(Math.round(this.cents / divisor), this.currency);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Distribute money evenly without losing cents
|
|
65
|
+
* The remainder is distributed to the first parts
|
|
66
|
+
* @example $100 / 3 = [$33.34, $33.33, $33.33]
|
|
67
|
+
*/
|
|
68
|
+
distribute(parts) {
|
|
69
|
+
if (parts <= 0 || !Number.isInteger(parts)) {
|
|
70
|
+
throw new Error("Parts must be a positive integer");
|
|
71
|
+
}
|
|
72
|
+
const quotient = Math.floor(this.cents / parts);
|
|
73
|
+
const remainder = this.cents % parts;
|
|
74
|
+
const result = [];
|
|
75
|
+
for (let i = 0; i < parts; i++) {
|
|
76
|
+
const extra = i < remainder ? 1 : 0;
|
|
77
|
+
result.push(new _Money(quotient + extra, this.currency));
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Distribute money according to ratios
|
|
83
|
+
* @example $100 with ratios [1, 2, 2] = [$20, $40, $40]
|
|
84
|
+
*/
|
|
85
|
+
distributeByRatios(ratios) {
|
|
86
|
+
if (ratios.length === 0) {
|
|
87
|
+
throw new Error("Ratios array cannot be empty");
|
|
88
|
+
}
|
|
89
|
+
const total = ratios.reduce((sum, r) => sum + r, 0);
|
|
90
|
+
if (total <= 0) {
|
|
91
|
+
throw new Error("Sum of ratios must be positive");
|
|
92
|
+
}
|
|
93
|
+
let remaining = this.cents;
|
|
94
|
+
const result = [];
|
|
95
|
+
for (let i = 0; i < ratios.length; i++) {
|
|
96
|
+
if (i === ratios.length - 1) {
|
|
97
|
+
result.push(new _Money(remaining, this.currency));
|
|
98
|
+
} else {
|
|
99
|
+
const share = Math.round(this.cents * ratios[i] / total);
|
|
100
|
+
result.push(new _Money(share, this.currency));
|
|
101
|
+
remaining -= share;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Format money for display
|
|
108
|
+
*/
|
|
109
|
+
format(options = {}) {
|
|
110
|
+
const { showSymbol = true, showDecimals = true, symbolPosition } = options;
|
|
111
|
+
const decimal = this.toDecimal();
|
|
112
|
+
const absValue = Math.abs(decimal);
|
|
113
|
+
const isNegative = decimal < 0;
|
|
114
|
+
let formatted;
|
|
115
|
+
if (showDecimals && this.currency.decimals > 0) {
|
|
116
|
+
const [intPart, decPart] = absValue.toFixed(this.currency.decimals).split(".");
|
|
117
|
+
const intFormatted = this.formatInteger(intPart);
|
|
118
|
+
formatted = `${intFormatted}${this.currency.decimalSeparator}${decPart}`;
|
|
119
|
+
} else {
|
|
120
|
+
formatted = this.formatInteger(Math.round(absValue).toString());
|
|
121
|
+
}
|
|
122
|
+
if (isNegative) {
|
|
123
|
+
formatted = `-${formatted}`;
|
|
124
|
+
}
|
|
125
|
+
if (showSymbol) {
|
|
126
|
+
const pos = symbolPosition ?? this.currency.symbolPosition;
|
|
127
|
+
const space = this.currency.symbolSpacing ? " " : "";
|
|
128
|
+
if (pos === "before") {
|
|
129
|
+
formatted = `${this.currency.symbol}${space}${formatted}`;
|
|
130
|
+
} else {
|
|
131
|
+
formatted = `${formatted}${space}${this.currency.symbol}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return formatted;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the decimal representation
|
|
138
|
+
*/
|
|
139
|
+
toDecimal() {
|
|
140
|
+
return this.cents / Math.pow(10, this.currency.decimals);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check equality with another money value
|
|
144
|
+
*/
|
|
145
|
+
equals(other) {
|
|
146
|
+
return this.cents === other.cents && this.currency.code === other.currency.code;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if greater than another money value
|
|
150
|
+
*/
|
|
151
|
+
greaterThan(other) {
|
|
152
|
+
this.assertSameCurrency(other);
|
|
153
|
+
return this.cents > other.cents;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if less than another money value
|
|
157
|
+
*/
|
|
158
|
+
lessThan(other) {
|
|
159
|
+
this.assertSameCurrency(other);
|
|
160
|
+
return this.cents < other.cents;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if greater than or equal
|
|
164
|
+
*/
|
|
165
|
+
greaterThanOrEqual(other) {
|
|
166
|
+
this.assertSameCurrency(other);
|
|
167
|
+
return this.cents >= other.cents;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if less than or equal
|
|
171
|
+
*/
|
|
172
|
+
lessThanOrEqual(other) {
|
|
173
|
+
this.assertSameCurrency(other);
|
|
174
|
+
return this.cents <= other.cents;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check if zero
|
|
178
|
+
*/
|
|
179
|
+
isZero() {
|
|
180
|
+
return this.cents === 0;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if positive
|
|
184
|
+
*/
|
|
185
|
+
isPositive() {
|
|
186
|
+
return this.cents > 0;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if negative
|
|
190
|
+
*/
|
|
191
|
+
isNegative() {
|
|
192
|
+
return this.cents < 0;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get absolute value
|
|
196
|
+
*/
|
|
197
|
+
abs() {
|
|
198
|
+
return new _Money(Math.abs(this.cents), this.currency);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Negate the value
|
|
202
|
+
*/
|
|
203
|
+
negate() {
|
|
204
|
+
return new _Money(-this.cents, this.currency);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Convert to JSON-serializable object
|
|
208
|
+
*/
|
|
209
|
+
toJSON() {
|
|
210
|
+
return {
|
|
211
|
+
cents: this.cents,
|
|
212
|
+
currency: this.currency.code
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* String representation
|
|
217
|
+
*/
|
|
218
|
+
toString() {
|
|
219
|
+
return this.format();
|
|
220
|
+
}
|
|
221
|
+
formatInteger(intStr) {
|
|
222
|
+
const parts = [];
|
|
223
|
+
let remaining = intStr;
|
|
224
|
+
while (remaining.length > 3) {
|
|
225
|
+
parts.unshift(remaining.slice(-3));
|
|
226
|
+
remaining = remaining.slice(0, -3);
|
|
227
|
+
}
|
|
228
|
+
if (remaining) {
|
|
229
|
+
parts.unshift(remaining);
|
|
230
|
+
}
|
|
231
|
+
return parts.join(this.currency.thousandsSeparator);
|
|
232
|
+
}
|
|
233
|
+
assertSameCurrency(other) {
|
|
234
|
+
if (this.currency.code !== other.currency.code) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Cannot perform operation with different currencies: ${this.currency.code} and ${other.currency.code}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
exports.Money = Money;
|
|
243
|
+
//# sourceMappingURL=index.cjs.map
|
|
244
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/money.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Currency, FormatOptions, IMoney } from './types.js';\n\n/**\n * Immutable Money class that uses integer arithmetic for precision\n */\nexport class Money implements IMoney {\n readonly cents: number;\n readonly currency: Currency;\n\n private constructor(cents: number, currency: Currency) {\n if (!Number.isInteger(cents)) {\n throw new Error('Money must be created with an integer number of cents');\n }\n this.cents = cents;\n this.currency = currency;\n Object.freeze(this);\n }\n\n /**\n * Create money from cents (smallest unit)\n */\n static fromCents(cents: number, currency: Currency): Money {\n return new Money(Math.round(cents), currency);\n }\n\n /**\n * Create money from a decimal amount\n * @example Money.fromDecimal(100.50, USD) creates $100.50\n */\n static fromDecimal(amount: number, currency: Currency): Money {\n const multiplier = Math.pow(10, currency.decimals);\n const cents = Math.round(amount * multiplier);\n return new Money(cents, currency);\n }\n\n /**\n * Create zero money\n */\n static zero(currency: Currency): Money {\n return new Money(0, currency);\n }\n\n /**\n * Add another money value (must be same currency)\n */\n add(other: IMoney): Money {\n this.assertSameCurrency(other);\n return new Money(this.cents + other.cents, this.currency);\n }\n\n /**\n * Subtract another money value (must be same currency)\n */\n subtract(other: IMoney): Money {\n this.assertSameCurrency(other);\n return new Money(this.cents - other.cents, this.currency);\n }\n\n /**\n * Multiply by a factor (rounds to nearest cent)\n */\n multiply(factor: number): Money {\n return new Money(Math.round(this.cents * factor), this.currency);\n }\n\n /**\n * Divide by a divisor (rounds to nearest cent)\n */\n divide(divisor: number): Money {\n if (divisor === 0) {\n throw new Error('Cannot divide by zero');\n }\n return new Money(Math.round(this.cents / divisor), this.currency);\n }\n\n /**\n * Distribute money evenly without losing cents\n * The remainder is distributed to the first parts\n * @example $100 / 3 = [$33.34, $33.33, $33.33]\n */\n distribute(parts: number): Money[] {\n if (parts <= 0 || !Number.isInteger(parts)) {\n throw new Error('Parts must be a positive integer');\n }\n\n const quotient = Math.floor(this.cents / parts);\n const remainder = this.cents % parts;\n\n const result: Money[] = [];\n for (let i = 0; i < parts; i++) {\n // Add 1 cent to the first 'remainder' parts\n const extra = i < remainder ? 1 : 0;\n result.push(new Money(quotient + extra, this.currency));\n }\n\n return result;\n }\n\n /**\n * Distribute money according to ratios\n * @example $100 with ratios [1, 2, 2] = [$20, $40, $40]\n */\n distributeByRatios(ratios: number[]): Money[] {\n if (ratios.length === 0) {\n throw new Error('Ratios array cannot be empty');\n }\n\n const total = ratios.reduce((sum, r) => sum + r, 0);\n if (total <= 0) {\n throw new Error('Sum of ratios must be positive');\n }\n\n let remaining = this.cents;\n const result: Money[] = [];\n\n for (let i = 0; i < ratios.length; i++) {\n if (i === ratios.length - 1) {\n // Last part gets whatever is remaining to avoid rounding errors\n result.push(new Money(remaining, this.currency));\n } else {\n const share = Math.round((this.cents * ratios[i]) / total);\n result.push(new Money(share, this.currency));\n remaining -= share;\n }\n }\n\n return result;\n }\n\n /**\n * Format money for display\n */\n format(options: FormatOptions = {}): string {\n const { showSymbol = true, showDecimals = true, symbolPosition } = options;\n\n const decimal = this.toDecimal();\n const absValue = Math.abs(decimal);\n const isNegative = decimal < 0;\n\n // Format the number\n let formatted: string;\n if (showDecimals && this.currency.decimals > 0) {\n const [intPart, decPart] = absValue.toFixed(this.currency.decimals).split('.');\n const intFormatted = this.formatInteger(intPart);\n formatted = `${intFormatted}${this.currency.decimalSeparator}${decPart}`;\n } else {\n formatted = this.formatInteger(Math.round(absValue).toString());\n }\n\n // Add negative sign\n if (isNegative) {\n formatted = `-${formatted}`;\n }\n\n // Add symbol\n if (showSymbol) {\n const pos = symbolPosition ?? this.currency.symbolPosition;\n const space = this.currency.symbolSpacing ? ' ' : '';\n\n if (pos === 'before') {\n formatted = `${this.currency.symbol}${space}${formatted}`;\n } else {\n formatted = `${formatted}${space}${this.currency.symbol}`;\n }\n }\n\n return formatted;\n }\n\n /**\n * Get the decimal representation\n */\n toDecimal(): number {\n return this.cents / Math.pow(10, this.currency.decimals);\n }\n\n /**\n * Check equality with another money value\n */\n equals(other: IMoney): boolean {\n return this.cents === other.cents && this.currency.code === other.currency.code;\n }\n\n /**\n * Check if greater than another money value\n */\n greaterThan(other: IMoney): boolean {\n this.assertSameCurrency(other);\n return this.cents > other.cents;\n }\n\n /**\n * Check if less than another money value\n */\n lessThan(other: IMoney): boolean {\n this.assertSameCurrency(other);\n return this.cents < other.cents;\n }\n\n /**\n * Check if greater than or equal\n */\n greaterThanOrEqual(other: IMoney): boolean {\n this.assertSameCurrency(other);\n return this.cents >= other.cents;\n }\n\n /**\n * Check if less than or equal\n */\n lessThanOrEqual(other: IMoney): boolean {\n this.assertSameCurrency(other);\n return this.cents <= other.cents;\n }\n\n /**\n * Check if zero\n */\n isZero(): boolean {\n return this.cents === 0;\n }\n\n /**\n * Check if positive\n */\n isPositive(): boolean {\n return this.cents > 0;\n }\n\n /**\n * Check if negative\n */\n isNegative(): boolean {\n return this.cents < 0;\n }\n\n /**\n * Get absolute value\n */\n abs(): Money {\n return new Money(Math.abs(this.cents), this.currency);\n }\n\n /**\n * Negate the value\n */\n negate(): Money {\n return new Money(-this.cents, this.currency);\n }\n\n /**\n * Convert to JSON-serializable object\n */\n toJSON(): { cents: number; currency: string } {\n return {\n cents: this.cents,\n currency: this.currency.code,\n };\n }\n\n /**\n * String representation\n */\n toString(): string {\n return this.format();\n }\n\n private formatInteger(intStr: string): string {\n const parts: string[] = [];\n let remaining = intStr;\n\n while (remaining.length > 3) {\n parts.unshift(remaining.slice(-3));\n remaining = remaining.slice(0, -3);\n }\n\n if (remaining) {\n parts.unshift(remaining);\n }\n\n return parts.join(this.currency.thousandsSeparator);\n }\n\n private assertSameCurrency(other: IMoney): void {\n if (this.currency.code !== other.currency.code) {\n throw new Error(\n `Cannot perform operation with different currencies: ${this.currency.code} and ${other.currency.code}`\n );\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency configuration for a specific locale
|
|
3
|
+
*/
|
|
4
|
+
interface Currency {
|
|
5
|
+
/** ISO 4217 currency code (e.g., 'COP', 'USD') */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Currency symbol (e.g., '$', 'R$') */
|
|
8
|
+
symbol: string;
|
|
9
|
+
/** Number of decimal places (usually 2, but some currencies use 0) */
|
|
10
|
+
decimals: number;
|
|
11
|
+
/** Thousands separator (e.g., '.' for CO, ',' for US) */
|
|
12
|
+
thousandsSeparator: string;
|
|
13
|
+
/** Decimal separator (e.g., ',' for CO, '.' for US) */
|
|
14
|
+
decimalSeparator: string;
|
|
15
|
+
/** Symbol position: 'before' or 'after' the amount */
|
|
16
|
+
symbolPosition: 'before' | 'after';
|
|
17
|
+
/** Space between symbol and amount */
|
|
18
|
+
symbolSpacing: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Options for formatting money
|
|
22
|
+
*/
|
|
23
|
+
interface FormatOptions {
|
|
24
|
+
/** Whether to show the currency symbol */
|
|
25
|
+
showSymbol?: boolean;
|
|
26
|
+
/** Whether to show decimal places */
|
|
27
|
+
showDecimals?: boolean;
|
|
28
|
+
/** Override the default symbol position */
|
|
29
|
+
symbolPosition?: 'before' | 'after';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Result of a money distribution operation
|
|
33
|
+
*/
|
|
34
|
+
type DistributionResult = Money[];
|
|
35
|
+
/**
|
|
36
|
+
* Represents an immutable monetary value with safe integer arithmetic
|
|
37
|
+
*/
|
|
38
|
+
interface IMoney {
|
|
39
|
+
/** The amount in the smallest unit (cents) */
|
|
40
|
+
readonly cents: number;
|
|
41
|
+
/** The currency configuration */
|
|
42
|
+
readonly currency: Currency;
|
|
43
|
+
/** Add another money value */
|
|
44
|
+
add(other: IMoney): IMoney;
|
|
45
|
+
/** Subtract another money value */
|
|
46
|
+
subtract(other: IMoney): IMoney;
|
|
47
|
+
/** Multiply by a factor */
|
|
48
|
+
multiply(factor: number): IMoney;
|
|
49
|
+
/** Divide by a divisor */
|
|
50
|
+
divide(divisor: number): IMoney;
|
|
51
|
+
/** Distribute evenly without losing cents */
|
|
52
|
+
distribute(parts: number): IMoney[];
|
|
53
|
+
/** Format for display */
|
|
54
|
+
format(options?: FormatOptions): string;
|
|
55
|
+
/** Get the decimal amount */
|
|
56
|
+
toDecimal(): number;
|
|
57
|
+
/** Check equality */
|
|
58
|
+
equals(other: IMoney): boolean;
|
|
59
|
+
/** Check if greater than */
|
|
60
|
+
greaterThan(other: IMoney): boolean;
|
|
61
|
+
/** Check if less than */
|
|
62
|
+
lessThan(other: IMoney): boolean;
|
|
63
|
+
/** Check if zero */
|
|
64
|
+
isZero(): boolean;
|
|
65
|
+
/** Check if positive */
|
|
66
|
+
isPositive(): boolean;
|
|
67
|
+
/** Check if negative */
|
|
68
|
+
isNegative(): boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Immutable Money class that uses integer arithmetic for precision
|
|
73
|
+
*/
|
|
74
|
+
declare class Money implements IMoney {
|
|
75
|
+
readonly cents: number;
|
|
76
|
+
readonly currency: Currency;
|
|
77
|
+
private constructor();
|
|
78
|
+
/**
|
|
79
|
+
* Create money from cents (smallest unit)
|
|
80
|
+
*/
|
|
81
|
+
static fromCents(cents: number, currency: Currency): Money;
|
|
82
|
+
/**
|
|
83
|
+
* Create money from a decimal amount
|
|
84
|
+
* @example Money.fromDecimal(100.50, USD) creates $100.50
|
|
85
|
+
*/
|
|
86
|
+
static fromDecimal(amount: number, currency: Currency): Money;
|
|
87
|
+
/**
|
|
88
|
+
* Create zero money
|
|
89
|
+
*/
|
|
90
|
+
static zero(currency: Currency): Money;
|
|
91
|
+
/**
|
|
92
|
+
* Add another money value (must be same currency)
|
|
93
|
+
*/
|
|
94
|
+
add(other: IMoney): Money;
|
|
95
|
+
/**
|
|
96
|
+
* Subtract another money value (must be same currency)
|
|
97
|
+
*/
|
|
98
|
+
subtract(other: IMoney): Money;
|
|
99
|
+
/**
|
|
100
|
+
* Multiply by a factor (rounds to nearest cent)
|
|
101
|
+
*/
|
|
102
|
+
multiply(factor: number): Money;
|
|
103
|
+
/**
|
|
104
|
+
* Divide by a divisor (rounds to nearest cent)
|
|
105
|
+
*/
|
|
106
|
+
divide(divisor: number): Money;
|
|
107
|
+
/**
|
|
108
|
+
* Distribute money evenly without losing cents
|
|
109
|
+
* The remainder is distributed to the first parts
|
|
110
|
+
* @example $100 / 3 = [$33.34, $33.33, $33.33]
|
|
111
|
+
*/
|
|
112
|
+
distribute(parts: number): Money[];
|
|
113
|
+
/**
|
|
114
|
+
* Distribute money according to ratios
|
|
115
|
+
* @example $100 with ratios [1, 2, 2] = [$20, $40, $40]
|
|
116
|
+
*/
|
|
117
|
+
distributeByRatios(ratios: number[]): Money[];
|
|
118
|
+
/**
|
|
119
|
+
* Format money for display
|
|
120
|
+
*/
|
|
121
|
+
format(options?: FormatOptions): string;
|
|
122
|
+
/**
|
|
123
|
+
* Get the decimal representation
|
|
124
|
+
*/
|
|
125
|
+
toDecimal(): number;
|
|
126
|
+
/**
|
|
127
|
+
* Check equality with another money value
|
|
128
|
+
*/
|
|
129
|
+
equals(other: IMoney): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Check if greater than another money value
|
|
132
|
+
*/
|
|
133
|
+
greaterThan(other: IMoney): boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Check if less than another money value
|
|
136
|
+
*/
|
|
137
|
+
lessThan(other: IMoney): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Check if greater than or equal
|
|
140
|
+
*/
|
|
141
|
+
greaterThanOrEqual(other: IMoney): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Check if less than or equal
|
|
144
|
+
*/
|
|
145
|
+
lessThanOrEqual(other: IMoney): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Check if zero
|
|
148
|
+
*/
|
|
149
|
+
isZero(): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Check if positive
|
|
152
|
+
*/
|
|
153
|
+
isPositive(): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Check if negative
|
|
156
|
+
*/
|
|
157
|
+
isNegative(): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Get absolute value
|
|
160
|
+
*/
|
|
161
|
+
abs(): Money;
|
|
162
|
+
/**
|
|
163
|
+
* Negate the value
|
|
164
|
+
*/
|
|
165
|
+
negate(): Money;
|
|
166
|
+
/**
|
|
167
|
+
* Convert to JSON-serializable object
|
|
168
|
+
*/
|
|
169
|
+
toJSON(): {
|
|
170
|
+
cents: number;
|
|
171
|
+
currency: string;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* String representation
|
|
175
|
+
*/
|
|
176
|
+
toString(): string;
|
|
177
|
+
private formatInteger;
|
|
178
|
+
private assertSameCurrency;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { type Currency, type DistributionResult, type FormatOptions, type IMoney, Money };
|