@lacspace/money 1.0.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 +51 -0
- package/README.md +75 -0
- package/dist/index.cjs +208 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +203 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Lacspace Free Licence
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Lacspace
|
|
5
|
+
|
|
6
|
+
PREAMBLE
|
|
7
|
+
|
|
8
|
+
This software is published by Lacspace under the Lacspace Free Licence — a free,
|
|
9
|
+
permissive licence that lets you use this software for any purpose, including in
|
|
10
|
+
commercial products and services, at no cost. It grants the same freedoms as
|
|
11
|
+
common permissive open-source licences; the only condition is that this notice
|
|
12
|
+
travels with the software. The canonical, always-current text of this licence is
|
|
13
|
+
maintained at https://lacspace.com/licenses/lacspace-free-1.0
|
|
14
|
+
|
|
15
|
+
GRANT OF RIGHTS
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person or organisation
|
|
18
|
+
obtaining a copy of this software and its associated documentation and data files
|
|
19
|
+
(the "Software"), to deal in the Software without restriction, including without
|
|
20
|
+
limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
21
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
22
|
+
Software is furnished to do so, subject to the conditions below. These rights are
|
|
23
|
+
granted for any purpose, personal or commercial, and are perpetual, worldwide,
|
|
24
|
+
non-exclusive, and royalty-free.
|
|
25
|
+
|
|
26
|
+
CONDITIONS
|
|
27
|
+
|
|
28
|
+
The above copyright notice, this permission notice, and the name of this licence
|
|
29
|
+
("Lacspace Free Licence") shall be included in all copies or substantial portions
|
|
30
|
+
of the Software.
|
|
31
|
+
|
|
32
|
+
TRADEMARKS
|
|
33
|
+
|
|
34
|
+
This licence does not grant permission to use the trade names, trademarks, service
|
|
35
|
+
marks, logos, or product names of Lacspace, except as required to reproduce the
|
|
36
|
+
notice above or to describe the origin of the Software in a truthful manner.
|
|
37
|
+
|
|
38
|
+
DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
42
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
43
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
|
|
44
|
+
AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
|
|
45
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
The Lacspace Free Licence is a source-available, permissive licence and is not (as
|
|
50
|
+
of this version) an OSI-approved licence. In substance it grants the same freedoms
|
|
51
|
+
as the MIT Licence. Learn more at https://lacspace.com/licenses
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/money
|
|
4
|
+
|
|
5
|
+
**Money without the floating-point bugs — integer cents, safe math, localized formatting.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/money)
|
|
8
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
> `0.1 + 0.2 !== 0.3` — so never store money as a float. `@lacspace/money` keeps amounts as **integer minor units**, refuses to add different currencies, splits a bill without losing a cent, and formats with `Intl`. Tiny, typed, isomorphic.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i @lacspace/money
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use it
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { money, Money } from "@lacspace/money";
|
|
24
|
+
|
|
25
|
+
const price = money(19.99, "USD"); // 1999 minor units, exact
|
|
26
|
+
price.multiply(3).format(); // "$59.97"
|
|
27
|
+
price.add(money(5, "USD")); // $24.99
|
|
28
|
+
money(9.99, "USD").add(money(1, "EUR")); // ❌ throws: currency mismatch
|
|
29
|
+
|
|
30
|
+
// Split a bill three ways — the cent doesn't vanish
|
|
31
|
+
money(10, "USD").allocate([1, 1, 1]).map((m) => m.format());
|
|
32
|
+
// ["$3.34", "$3.33", "$3.33"] (sum is exactly $10.00)
|
|
33
|
+
|
|
34
|
+
// Zero-decimal & 3-decimal currencies handled automatically
|
|
35
|
+
money(1000, "JPY").format("ja-JP"); // "¥1,000"
|
|
36
|
+
money(1.5, "BHD").toMinor(); // 1500 (BHD has 3 decimals)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why minor units
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
Money.fromMinor(1999, "USD"); // exact, no rounding surprises
|
|
43
|
+
Money.of(19.99, "USD"); // convenience: rounds major → minor once
|
|
44
|
+
money(19.99, "USD").toMinor(); // 1999
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
| | |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `money(major, ccy)` / `Money.of` / `Money.fromMinor` / `Money.zero` / `Money.parse` | construct |
|
|
52
|
+
| `.add` · `.subtract` · `.multiply` · `.divide` · `.negate` · `.abs` | arithmetic (currency-checked) |
|
|
53
|
+
| `.allocate(ratios)` · `.split(n)` | remainder-preserving distribution |
|
|
54
|
+
| `.equals` · `.greaterThan` · `.lessThan` · `.greaterThanOrEqual` · `.lessThanOrEqual` | compare |
|
|
55
|
+
| `.isZero` · `.isPositive` · `.isNegative` | predicates |
|
|
56
|
+
| `.format(locale?, opts?)` · `.toString` · `.toMajor` · `.toMinor` · `.toJSON` | output |
|
|
57
|
+
| `sumMoney(list, ccy?)` · `decimalsFor(ccy)` | helpers |
|
|
58
|
+
|
|
59
|
+
All operations return a new `Money` — instances are immutable.
|
|
60
|
+
|
|
61
|
+
## Licensing
|
|
62
|
+
|
|
63
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice. See the **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
<div align="center">
|
|
68
|
+
|
|
69
|
+
**Part of the Lacspace ecosystem — zero-dependency, isomorphic TypeScript packages.**
|
|
70
|
+
|
|
71
|
+
[All packages ↗](https://lacspace.com/packages) · [npm org ↗](https://www.npmjs.com/org/lacspace) · [Licence Centre ↗](https://lacspace.com/licenses) · [GitHub ↗](https://github.com/lacspace/npm-packages)
|
|
72
|
+
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var EXPONENTS = {
|
|
5
|
+
JPY: 0,
|
|
6
|
+
KRW: 0,
|
|
7
|
+
VND: 0,
|
|
8
|
+
CLP: 0,
|
|
9
|
+
ISK: 0,
|
|
10
|
+
HUF: 0,
|
|
11
|
+
XAF: 0,
|
|
12
|
+
XOF: 0,
|
|
13
|
+
XPF: 0,
|
|
14
|
+
RWF: 0,
|
|
15
|
+
UGX: 0,
|
|
16
|
+
GNF: 0,
|
|
17
|
+
BHD: 3,
|
|
18
|
+
KWD: 3,
|
|
19
|
+
OMR: 3,
|
|
20
|
+
TND: 3,
|
|
21
|
+
IQD: 3,
|
|
22
|
+
JOD: 3,
|
|
23
|
+
LYD: 3
|
|
24
|
+
};
|
|
25
|
+
function decimalsFor(currency) {
|
|
26
|
+
return EXPONENTS[currency.toUpperCase()] ?? 2;
|
|
27
|
+
}
|
|
28
|
+
function factorFor(currency) {
|
|
29
|
+
return Math.pow(10, decimalsFor(currency));
|
|
30
|
+
}
|
|
31
|
+
function roundHalfUp(n) {
|
|
32
|
+
return n < 0 ? -Math.round(-n) : Math.round(n);
|
|
33
|
+
}
|
|
34
|
+
var Money = class _Money {
|
|
35
|
+
constructor(minor, currency) {
|
|
36
|
+
if (!Number.isInteger(minor)) {
|
|
37
|
+
throw new TypeError(`Money amount must be an integer number of minor units, got ${minor}`);
|
|
38
|
+
}
|
|
39
|
+
this.amount = minor;
|
|
40
|
+
this.currency = currency.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
/** From major units: `Money.of(19.99, "USD")` → 1999 minor units. */
|
|
43
|
+
static of(major, currency) {
|
|
44
|
+
return new _Money(roundHalfUp(major * factorFor(currency)), currency);
|
|
45
|
+
}
|
|
46
|
+
/** From an integer count of minor units: `Money.fromMinor(1999, "USD")`. */
|
|
47
|
+
static fromMinor(minor, currency) {
|
|
48
|
+
return new _Money(minor, currency);
|
|
49
|
+
}
|
|
50
|
+
/** Zero in a currency. */
|
|
51
|
+
static zero(currency) {
|
|
52
|
+
return new _Money(0, currency);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse a formatted string like "$1,234.56" or "1.234,56" (best-effort).
|
|
56
|
+
* Non-digits except the last separator group are stripped.
|
|
57
|
+
*/
|
|
58
|
+
static parse(input, currency) {
|
|
59
|
+
const cleaned = input.replace(/[^\d.,-]/g, "").trim();
|
|
60
|
+
if (!cleaned) throw new Error(`Cannot parse money from "${input}"`);
|
|
61
|
+
const neg = /^-/.test(cleaned) || /-\d/.test(cleaned);
|
|
62
|
+
let s = cleaned.replace(/-/g, "");
|
|
63
|
+
const lastDot = s.lastIndexOf(".");
|
|
64
|
+
const lastComma = s.lastIndexOf(",");
|
|
65
|
+
let decSep = "";
|
|
66
|
+
if (lastDot >= 0 && lastComma >= 0) decSep = lastDot > lastComma ? "." : ",";
|
|
67
|
+
else if (lastDot >= 0) decSep = ".";
|
|
68
|
+
else if (lastComma >= 0) decSep = ",";
|
|
69
|
+
let major;
|
|
70
|
+
if (decSep) {
|
|
71
|
+
const thousandsSep = decSep === "." ? "," : ".";
|
|
72
|
+
s = s.split(thousandsSep).join("");
|
|
73
|
+
major = Number(s.replace(decSep, "."));
|
|
74
|
+
} else {
|
|
75
|
+
major = Number(s);
|
|
76
|
+
}
|
|
77
|
+
if (Number.isNaN(major)) throw new Error(`Cannot parse money from "${input}"`);
|
|
78
|
+
return _Money.of(neg ? -major : major, currency);
|
|
79
|
+
}
|
|
80
|
+
assertSame(other) {
|
|
81
|
+
if (other.currency !== this.currency) {
|
|
82
|
+
throw new Error(`Currency mismatch: ${this.currency} vs ${other.currency}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
add(other) {
|
|
86
|
+
this.assertSame(other);
|
|
87
|
+
return new _Money(this.amount + other.amount, this.currency);
|
|
88
|
+
}
|
|
89
|
+
subtract(other) {
|
|
90
|
+
this.assertSame(other);
|
|
91
|
+
return new _Money(this.amount - other.amount, this.currency);
|
|
92
|
+
}
|
|
93
|
+
/** Multiply by a scalar (e.g. quantity or tax rate), rounding to whole minor units. */
|
|
94
|
+
multiply(factor) {
|
|
95
|
+
return new _Money(roundHalfUp(this.amount * factor), this.currency);
|
|
96
|
+
}
|
|
97
|
+
/** Divide by a scalar, rounding to whole minor units. */
|
|
98
|
+
divide(divisor) {
|
|
99
|
+
if (divisor === 0) throw new Error("Division by zero");
|
|
100
|
+
return new _Money(roundHalfUp(this.amount / divisor), this.currency);
|
|
101
|
+
}
|
|
102
|
+
negate() {
|
|
103
|
+
return new _Money(-this.amount, this.currency);
|
|
104
|
+
}
|
|
105
|
+
abs() {
|
|
106
|
+
return new _Money(Math.abs(this.amount), this.currency);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Split into parts by integer ratios, distributing leftover minor units to the
|
|
110
|
+
* earliest parts so the sum always equals the original (no cent lost/created).
|
|
111
|
+
* `money(10,"USD").allocate([1,1,1])` → 3.34, 3.33, 3.33.
|
|
112
|
+
*/
|
|
113
|
+
allocate(ratios) {
|
|
114
|
+
if (ratios.length === 0) throw new Error("allocate() needs at least one ratio");
|
|
115
|
+
if (ratios.some((r) => r < 0)) throw new Error("allocate() ratios must be non-negative");
|
|
116
|
+
const total = ratios.reduce((a, b) => a + b, 0);
|
|
117
|
+
if (total === 0) throw new Error("allocate() ratios must not all be zero");
|
|
118
|
+
const shares = ratios.map((r) => Math.floor(this.amount * r / total));
|
|
119
|
+
let remainder = this.amount - shares.reduce((a, b) => a + b, 0);
|
|
120
|
+
const order = ratios.map((r, i) => ({ r, i })).sort((a, b) => b.r - a.r);
|
|
121
|
+
let k = 0;
|
|
122
|
+
while (remainder > 0) {
|
|
123
|
+
shares[order[k % order.length].i] += 1;
|
|
124
|
+
remainder--;
|
|
125
|
+
k++;
|
|
126
|
+
}
|
|
127
|
+
return shares.map((s) => new _Money(s, this.currency));
|
|
128
|
+
}
|
|
129
|
+
/** Split evenly into `n` parts, distributing the remainder fairly. */
|
|
130
|
+
split(n) {
|
|
131
|
+
if (!Number.isInteger(n) || n <= 0) throw new Error("split() needs a positive integer");
|
|
132
|
+
return this.allocate(new Array(n).fill(1));
|
|
133
|
+
}
|
|
134
|
+
equals(other) {
|
|
135
|
+
return this.currency === other.currency && this.amount === other.amount;
|
|
136
|
+
}
|
|
137
|
+
greaterThan(other) {
|
|
138
|
+
this.assertSame(other);
|
|
139
|
+
return this.amount > other.amount;
|
|
140
|
+
}
|
|
141
|
+
lessThan(other) {
|
|
142
|
+
this.assertSame(other);
|
|
143
|
+
return this.amount < other.amount;
|
|
144
|
+
}
|
|
145
|
+
greaterThanOrEqual(other) {
|
|
146
|
+
this.assertSame(other);
|
|
147
|
+
return this.amount >= other.amount;
|
|
148
|
+
}
|
|
149
|
+
lessThanOrEqual(other) {
|
|
150
|
+
this.assertSame(other);
|
|
151
|
+
return this.amount <= other.amount;
|
|
152
|
+
}
|
|
153
|
+
isZero() {
|
|
154
|
+
return this.amount === 0;
|
|
155
|
+
}
|
|
156
|
+
isPositive() {
|
|
157
|
+
return this.amount > 0;
|
|
158
|
+
}
|
|
159
|
+
isNegative() {
|
|
160
|
+
return this.amount < 0;
|
|
161
|
+
}
|
|
162
|
+
/** The value in major units as a number (e.g. 19.99). Beware float for display — prefer format(). */
|
|
163
|
+
toMajor() {
|
|
164
|
+
return this.amount / factorFor(this.currency);
|
|
165
|
+
}
|
|
166
|
+
/** The raw integer minor-unit amount. */
|
|
167
|
+
toMinor() {
|
|
168
|
+
return this.amount;
|
|
169
|
+
}
|
|
170
|
+
/** Localized currency string, e.g. "$1,234.56" / "€1.234,56". */
|
|
171
|
+
format(locale, options) {
|
|
172
|
+
const decimals = decimalsFor(this.currency);
|
|
173
|
+
try {
|
|
174
|
+
return new Intl.NumberFormat(locale, {
|
|
175
|
+
style: "currency",
|
|
176
|
+
currency: this.currency,
|
|
177
|
+
minimumFractionDigits: decimals,
|
|
178
|
+
maximumFractionDigits: decimals,
|
|
179
|
+
...options
|
|
180
|
+
}).format(this.toMajor());
|
|
181
|
+
} catch {
|
|
182
|
+
return `${this.toMajor().toFixed(decimals)} ${this.currency}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
toString() {
|
|
186
|
+
return this.format();
|
|
187
|
+
}
|
|
188
|
+
toJSON() {
|
|
189
|
+
return { amount: this.amount, currency: this.currency };
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function money(major, currency) {
|
|
193
|
+
return Money.of(major, currency);
|
|
194
|
+
}
|
|
195
|
+
function sumMoney(items, currency) {
|
|
196
|
+
if (items.length === 0) {
|
|
197
|
+
if (!currency) throw new Error("sumMoney() of an empty list needs a currency");
|
|
198
|
+
return Money.zero(currency);
|
|
199
|
+
}
|
|
200
|
+
return items.reduce((acc, m) => acc.add(m));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
exports.Money = Money;
|
|
204
|
+
exports.decimalsFor = decimalsFor;
|
|
205
|
+
exports.money = money;
|
|
206
|
+
exports.sumMoney = sumMoney;
|
|
207
|
+
//# sourceMappingURL=index.cjs.map
|
|
208
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/money\n * Money done right — integer minor units (no floating-point cents bugs),\n * currency-safe arithmetic, remainder-preserving allocation, and localized\n * formatting via `Intl`.\n *\n * ```ts\n * import { money } from \"@lacspace/money\";\n *\n * const price = money(19.99, \"USD\"); // 1999 minor units\n * const total = price.multiply(3); // $59.97, exact\n * total.format(); // \"$59.97\"\n * money(10, \"USD\").allocate([1, 1, 1]); // [$3.34, $3.33, $3.33] — no cent lost\n * ```\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\n/** Minor-unit exponents for currencies that aren't the default 2. */\nconst EXPONENTS: Record<string, number> = {\n JPY: 0, KRW: 0, VND: 0, CLP: 0, ISK: 0, HUF: 0, XAF: 0, XOF: 0, XPF: 0, RWF: 0, UGX: 0, GNF: 0,\n BHD: 3, KWD: 3, OMR: 3, TND: 3, IQD: 3, JOD: 3, LYD: 3,\n};\n\n/** How many minor units are in one major unit for a currency (e.g. 100 for USD). */\nexport function decimalsFor(currency: string): number {\n return EXPONENTS[currency.toUpperCase()] ?? 2;\n}\n\nfunction factorFor(currency: string): number {\n return Math.pow(10, decimalsFor(currency));\n}\n\n/** Round half away from zero — the intuitive rule for money. */\nfunction roundHalfUp(n: number): number {\n return n < 0 ? -Math.round(-n) : Math.round(n);\n}\n\nexport class Money {\n /** Integer amount in minor units (e.g. cents). */\n readonly amount: number;\n /** ISO 4217 code, upper-cased. */\n readonly currency: string;\n\n private constructor(minor: number, currency: string) {\n if (!Number.isInteger(minor)) {\n throw new TypeError(`Money amount must be an integer number of minor units, got ${minor}`);\n }\n this.amount = minor;\n this.currency = currency.toUpperCase();\n }\n\n /** From major units: `Money.of(19.99, \"USD\")` → 1999 minor units. */\n static of(major: number, currency: string): Money {\n return new Money(roundHalfUp(major * factorFor(currency)), currency);\n }\n\n /** From an integer count of minor units: `Money.fromMinor(1999, \"USD\")`. */\n static fromMinor(minor: number, currency: string): Money {\n return new Money(minor, currency);\n }\n\n /** Zero in a currency. */\n static zero(currency: string): Money {\n return new Money(0, currency);\n }\n\n /**\n * Parse a formatted string like \"$1,234.56\" or \"1.234,56\" (best-effort).\n * Non-digits except the last separator group are stripped.\n */\n static parse(input: string, currency: string): Money {\n const cleaned = input.replace(/[^\\d.,-]/g, \"\").trim();\n if (!cleaned) throw new Error(`Cannot parse money from \"${input}\"`);\n const neg = /^-/.test(cleaned) || /-\\d/.test(cleaned);\n let s = cleaned.replace(/-/g, \"\");\n // Decide the decimal separator: whichever of . or , comes last.\n const lastDot = s.lastIndexOf(\".\");\n const lastComma = s.lastIndexOf(\",\");\n let decSep = \"\";\n if (lastDot >= 0 && lastComma >= 0) decSep = lastDot > lastComma ? \".\" : \",\";\n else if (lastDot >= 0) decSep = \".\";\n else if (lastComma >= 0) decSep = \",\";\n let major: number;\n if (decSep) {\n const thousandsSep = decSep === \".\" ? \",\" : \".\";\n s = s.split(thousandsSep).join(\"\");\n major = Number(s.replace(decSep, \".\"));\n } else {\n major = Number(s);\n }\n if (Number.isNaN(major)) throw new Error(`Cannot parse money from \"${input}\"`);\n return Money.of(neg ? -major : major, currency);\n }\n\n private assertSame(other: Money): void {\n if (other.currency !== this.currency) {\n throw new Error(`Currency mismatch: ${this.currency} vs ${other.currency}`);\n }\n }\n\n add(other: Money): Money {\n this.assertSame(other);\n return new Money(this.amount + other.amount, this.currency);\n }\n\n subtract(other: Money): Money {\n this.assertSame(other);\n return new Money(this.amount - other.amount, this.currency);\n }\n\n /** Multiply by a scalar (e.g. quantity or tax rate), rounding to whole minor units. */\n multiply(factor: number): Money {\n return new Money(roundHalfUp(this.amount * factor), this.currency);\n }\n\n /** Divide by a scalar, rounding to whole minor units. */\n divide(divisor: number): Money {\n if (divisor === 0) throw new Error(\"Division by zero\");\n return new Money(roundHalfUp(this.amount / divisor), this.currency);\n }\n\n negate(): Money {\n return new Money(-this.amount, this.currency);\n }\n\n abs(): Money {\n return new Money(Math.abs(this.amount), this.currency);\n }\n\n /**\n * Split into parts by integer ratios, distributing leftover minor units to the\n * earliest parts so the sum always equals the original (no cent lost/created).\n * `money(10,\"USD\").allocate([1,1,1])` → 3.34, 3.33, 3.33.\n */\n allocate(ratios: number[]): Money[] {\n if (ratios.length === 0) throw new Error(\"allocate() needs at least one ratio\");\n if (ratios.some((r) => r < 0)) throw new Error(\"allocate() ratios must be non-negative\");\n const total = ratios.reduce((a, b) => a + b, 0);\n if (total === 0) throw new Error(\"allocate() ratios must not all be zero\");\n const shares = ratios.map((r) => Math.floor((this.amount * r) / total));\n let remainder = this.amount - shares.reduce((a, b) => a + b, 0);\n // Hand out the remaining units one at a time to the parts with the biggest ratio.\n const order = ratios.map((r, i) => ({ r, i })).sort((a, b) => b.r - a.r);\n let k = 0;\n while (remainder > 0) {\n shares[order[k % order.length]!.i]! += 1;\n remainder--;\n k++;\n }\n return shares.map((s) => new Money(s, this.currency));\n }\n\n /** Split evenly into `n` parts, distributing the remainder fairly. */\n split(n: number): Money[] {\n if (!Number.isInteger(n) || n <= 0) throw new Error(\"split() needs a positive integer\");\n return this.allocate(new Array(n).fill(1));\n }\n\n equals(other: Money): boolean {\n return this.currency === other.currency && this.amount === other.amount;\n }\n greaterThan(other: Money): boolean {\n this.assertSame(other);\n return this.amount > other.amount;\n }\n lessThan(other: Money): boolean {\n this.assertSame(other);\n return this.amount < other.amount;\n }\n greaterThanOrEqual(other: Money): boolean {\n this.assertSame(other);\n return this.amount >= other.amount;\n }\n lessThanOrEqual(other: Money): boolean {\n this.assertSame(other);\n return this.amount <= other.amount;\n }\n isZero(): boolean {\n return this.amount === 0;\n }\n isPositive(): boolean {\n return this.amount > 0;\n }\n isNegative(): boolean {\n return this.amount < 0;\n }\n\n /** The value in major units as a number (e.g. 19.99). Beware float for display — prefer format(). */\n toMajor(): number {\n return this.amount / factorFor(this.currency);\n }\n\n /** The raw integer minor-unit amount. */\n toMinor(): number {\n return this.amount;\n }\n\n /** Localized currency string, e.g. \"$1,234.56\" / \"€1.234,56\". */\n format(locale?: string, options?: Intl.NumberFormatOptions): string {\n const decimals = decimalsFor(this.currency);\n try {\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: this.currency,\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n ...options,\n }).format(this.toMajor());\n } catch {\n // Unknown currency to Intl — fall back to a plain number + code.\n return `${this.toMajor().toFixed(decimals)} ${this.currency}`;\n }\n }\n\n toString(): string {\n return this.format();\n }\n\n toJSON(): { amount: number; currency: string } {\n return { amount: this.amount, currency: this.currency };\n }\n}\n\n/** Friendly constructor from major units: `money(19.99, \"USD\")`. */\nexport function money(major: number, currency: string): Money {\n return Money.of(major, currency);\n}\n\n/** Sum a list of Money (all same currency). Empty list requires a currency fallback. */\nexport function sumMoney(items: Money[], currency?: string): Money {\n if (items.length === 0) {\n if (!currency) throw new Error(\"sumMoney() of an empty list needs a currency\");\n return Money.zero(currency);\n }\n return items.reduce((acc, m) => acc.add(m));\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/money
|
|
3
|
+
* Money done right — integer minor units (no floating-point cents bugs),
|
|
4
|
+
* currency-safe arithmetic, remainder-preserving allocation, and localized
|
|
5
|
+
* formatting via `Intl`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { money } from "@lacspace/money";
|
|
9
|
+
*
|
|
10
|
+
* const price = money(19.99, "USD"); // 1999 minor units
|
|
11
|
+
* const total = price.multiply(3); // $59.97, exact
|
|
12
|
+
* total.format(); // "$59.97"
|
|
13
|
+
* money(10, "USD").allocate([1, 1, 1]); // [$3.34, $3.33, $3.33] — no cent lost
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Zero dependencies · isomorphic · fully typed.
|
|
17
|
+
*/
|
|
18
|
+
/** How many minor units are in one major unit for a currency (e.g. 100 for USD). */
|
|
19
|
+
declare function decimalsFor(currency: string): number;
|
|
20
|
+
declare class Money {
|
|
21
|
+
/** Integer amount in minor units (e.g. cents). */
|
|
22
|
+
readonly amount: number;
|
|
23
|
+
/** ISO 4217 code, upper-cased. */
|
|
24
|
+
readonly currency: string;
|
|
25
|
+
private constructor();
|
|
26
|
+
/** From major units: `Money.of(19.99, "USD")` → 1999 minor units. */
|
|
27
|
+
static of(major: number, currency: string): Money;
|
|
28
|
+
/** From an integer count of minor units: `Money.fromMinor(1999, "USD")`. */
|
|
29
|
+
static fromMinor(minor: number, currency: string): Money;
|
|
30
|
+
/** Zero in a currency. */
|
|
31
|
+
static zero(currency: string): Money;
|
|
32
|
+
/**
|
|
33
|
+
* Parse a formatted string like "$1,234.56" or "1.234,56" (best-effort).
|
|
34
|
+
* Non-digits except the last separator group are stripped.
|
|
35
|
+
*/
|
|
36
|
+
static parse(input: string, currency: string): Money;
|
|
37
|
+
private assertSame;
|
|
38
|
+
add(other: Money): Money;
|
|
39
|
+
subtract(other: Money): Money;
|
|
40
|
+
/** Multiply by a scalar (e.g. quantity or tax rate), rounding to whole minor units. */
|
|
41
|
+
multiply(factor: number): Money;
|
|
42
|
+
/** Divide by a scalar, rounding to whole minor units. */
|
|
43
|
+
divide(divisor: number): Money;
|
|
44
|
+
negate(): Money;
|
|
45
|
+
abs(): Money;
|
|
46
|
+
/**
|
|
47
|
+
* Split into parts by integer ratios, distributing leftover minor units to the
|
|
48
|
+
* earliest parts so the sum always equals the original (no cent lost/created).
|
|
49
|
+
* `money(10,"USD").allocate([1,1,1])` → 3.34, 3.33, 3.33.
|
|
50
|
+
*/
|
|
51
|
+
allocate(ratios: number[]): Money[];
|
|
52
|
+
/** Split evenly into `n` parts, distributing the remainder fairly. */
|
|
53
|
+
split(n: number): Money[];
|
|
54
|
+
equals(other: Money): boolean;
|
|
55
|
+
greaterThan(other: Money): boolean;
|
|
56
|
+
lessThan(other: Money): boolean;
|
|
57
|
+
greaterThanOrEqual(other: Money): boolean;
|
|
58
|
+
lessThanOrEqual(other: Money): boolean;
|
|
59
|
+
isZero(): boolean;
|
|
60
|
+
isPositive(): boolean;
|
|
61
|
+
isNegative(): boolean;
|
|
62
|
+
/** The value in major units as a number (e.g. 19.99). Beware float for display — prefer format(). */
|
|
63
|
+
toMajor(): number;
|
|
64
|
+
/** The raw integer minor-unit amount. */
|
|
65
|
+
toMinor(): number;
|
|
66
|
+
/** Localized currency string, e.g. "$1,234.56" / "€1.234,56". */
|
|
67
|
+
format(locale?: string, options?: Intl.NumberFormatOptions): string;
|
|
68
|
+
toString(): string;
|
|
69
|
+
toJSON(): {
|
|
70
|
+
amount: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Friendly constructor from major units: `money(19.99, "USD")`. */
|
|
75
|
+
declare function money(major: number, currency: string): Money;
|
|
76
|
+
/** Sum a list of Money (all same currency). Empty list requires a currency fallback. */
|
|
77
|
+
declare function sumMoney(items: Money[], currency?: string): Money;
|
|
78
|
+
|
|
79
|
+
export { Money, decimalsFor, money, sumMoney };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/money
|
|
3
|
+
* Money done right — integer minor units (no floating-point cents bugs),
|
|
4
|
+
* currency-safe arithmetic, remainder-preserving allocation, and localized
|
|
5
|
+
* formatting via `Intl`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { money } from "@lacspace/money";
|
|
9
|
+
*
|
|
10
|
+
* const price = money(19.99, "USD"); // 1999 minor units
|
|
11
|
+
* const total = price.multiply(3); // $59.97, exact
|
|
12
|
+
* total.format(); // "$59.97"
|
|
13
|
+
* money(10, "USD").allocate([1, 1, 1]); // [$3.34, $3.33, $3.33] — no cent lost
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Zero dependencies · isomorphic · fully typed.
|
|
17
|
+
*/
|
|
18
|
+
/** How many minor units are in one major unit for a currency (e.g. 100 for USD). */
|
|
19
|
+
declare function decimalsFor(currency: string): number;
|
|
20
|
+
declare class Money {
|
|
21
|
+
/** Integer amount in minor units (e.g. cents). */
|
|
22
|
+
readonly amount: number;
|
|
23
|
+
/** ISO 4217 code, upper-cased. */
|
|
24
|
+
readonly currency: string;
|
|
25
|
+
private constructor();
|
|
26
|
+
/** From major units: `Money.of(19.99, "USD")` → 1999 minor units. */
|
|
27
|
+
static of(major: number, currency: string): Money;
|
|
28
|
+
/** From an integer count of minor units: `Money.fromMinor(1999, "USD")`. */
|
|
29
|
+
static fromMinor(minor: number, currency: string): Money;
|
|
30
|
+
/** Zero in a currency. */
|
|
31
|
+
static zero(currency: string): Money;
|
|
32
|
+
/**
|
|
33
|
+
* Parse a formatted string like "$1,234.56" or "1.234,56" (best-effort).
|
|
34
|
+
* Non-digits except the last separator group are stripped.
|
|
35
|
+
*/
|
|
36
|
+
static parse(input: string, currency: string): Money;
|
|
37
|
+
private assertSame;
|
|
38
|
+
add(other: Money): Money;
|
|
39
|
+
subtract(other: Money): Money;
|
|
40
|
+
/** Multiply by a scalar (e.g. quantity or tax rate), rounding to whole minor units. */
|
|
41
|
+
multiply(factor: number): Money;
|
|
42
|
+
/** Divide by a scalar, rounding to whole minor units. */
|
|
43
|
+
divide(divisor: number): Money;
|
|
44
|
+
negate(): Money;
|
|
45
|
+
abs(): Money;
|
|
46
|
+
/**
|
|
47
|
+
* Split into parts by integer ratios, distributing leftover minor units to the
|
|
48
|
+
* earliest parts so the sum always equals the original (no cent lost/created).
|
|
49
|
+
* `money(10,"USD").allocate([1,1,1])` → 3.34, 3.33, 3.33.
|
|
50
|
+
*/
|
|
51
|
+
allocate(ratios: number[]): Money[];
|
|
52
|
+
/** Split evenly into `n` parts, distributing the remainder fairly. */
|
|
53
|
+
split(n: number): Money[];
|
|
54
|
+
equals(other: Money): boolean;
|
|
55
|
+
greaterThan(other: Money): boolean;
|
|
56
|
+
lessThan(other: Money): boolean;
|
|
57
|
+
greaterThanOrEqual(other: Money): boolean;
|
|
58
|
+
lessThanOrEqual(other: Money): boolean;
|
|
59
|
+
isZero(): boolean;
|
|
60
|
+
isPositive(): boolean;
|
|
61
|
+
isNegative(): boolean;
|
|
62
|
+
/** The value in major units as a number (e.g. 19.99). Beware float for display — prefer format(). */
|
|
63
|
+
toMajor(): number;
|
|
64
|
+
/** The raw integer minor-unit amount. */
|
|
65
|
+
toMinor(): number;
|
|
66
|
+
/** Localized currency string, e.g. "$1,234.56" / "€1.234,56". */
|
|
67
|
+
format(locale?: string, options?: Intl.NumberFormatOptions): string;
|
|
68
|
+
toString(): string;
|
|
69
|
+
toJSON(): {
|
|
70
|
+
amount: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Friendly constructor from major units: `money(19.99, "USD")`. */
|
|
75
|
+
declare function money(major: number, currency: string): Money;
|
|
76
|
+
/** Sum a list of Money (all same currency). Empty list requires a currency fallback. */
|
|
77
|
+
declare function sumMoney(items: Money[], currency?: string): Money;
|
|
78
|
+
|
|
79
|
+
export { Money, decimalsFor, money, sumMoney };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var EXPONENTS = {
|
|
3
|
+
JPY: 0,
|
|
4
|
+
KRW: 0,
|
|
5
|
+
VND: 0,
|
|
6
|
+
CLP: 0,
|
|
7
|
+
ISK: 0,
|
|
8
|
+
HUF: 0,
|
|
9
|
+
XAF: 0,
|
|
10
|
+
XOF: 0,
|
|
11
|
+
XPF: 0,
|
|
12
|
+
RWF: 0,
|
|
13
|
+
UGX: 0,
|
|
14
|
+
GNF: 0,
|
|
15
|
+
BHD: 3,
|
|
16
|
+
KWD: 3,
|
|
17
|
+
OMR: 3,
|
|
18
|
+
TND: 3,
|
|
19
|
+
IQD: 3,
|
|
20
|
+
JOD: 3,
|
|
21
|
+
LYD: 3
|
|
22
|
+
};
|
|
23
|
+
function decimalsFor(currency) {
|
|
24
|
+
return EXPONENTS[currency.toUpperCase()] ?? 2;
|
|
25
|
+
}
|
|
26
|
+
function factorFor(currency) {
|
|
27
|
+
return Math.pow(10, decimalsFor(currency));
|
|
28
|
+
}
|
|
29
|
+
function roundHalfUp(n) {
|
|
30
|
+
return n < 0 ? -Math.round(-n) : Math.round(n);
|
|
31
|
+
}
|
|
32
|
+
var Money = class _Money {
|
|
33
|
+
constructor(minor, currency) {
|
|
34
|
+
if (!Number.isInteger(minor)) {
|
|
35
|
+
throw new TypeError(`Money amount must be an integer number of minor units, got ${minor}`);
|
|
36
|
+
}
|
|
37
|
+
this.amount = minor;
|
|
38
|
+
this.currency = currency.toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
/** From major units: `Money.of(19.99, "USD")` → 1999 minor units. */
|
|
41
|
+
static of(major, currency) {
|
|
42
|
+
return new _Money(roundHalfUp(major * factorFor(currency)), currency);
|
|
43
|
+
}
|
|
44
|
+
/** From an integer count of minor units: `Money.fromMinor(1999, "USD")`. */
|
|
45
|
+
static fromMinor(minor, currency) {
|
|
46
|
+
return new _Money(minor, currency);
|
|
47
|
+
}
|
|
48
|
+
/** Zero in a currency. */
|
|
49
|
+
static zero(currency) {
|
|
50
|
+
return new _Money(0, currency);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse a formatted string like "$1,234.56" or "1.234,56" (best-effort).
|
|
54
|
+
* Non-digits except the last separator group are stripped.
|
|
55
|
+
*/
|
|
56
|
+
static parse(input, currency) {
|
|
57
|
+
const cleaned = input.replace(/[^\d.,-]/g, "").trim();
|
|
58
|
+
if (!cleaned) throw new Error(`Cannot parse money from "${input}"`);
|
|
59
|
+
const neg = /^-/.test(cleaned) || /-\d/.test(cleaned);
|
|
60
|
+
let s = cleaned.replace(/-/g, "");
|
|
61
|
+
const lastDot = s.lastIndexOf(".");
|
|
62
|
+
const lastComma = s.lastIndexOf(",");
|
|
63
|
+
let decSep = "";
|
|
64
|
+
if (lastDot >= 0 && lastComma >= 0) decSep = lastDot > lastComma ? "." : ",";
|
|
65
|
+
else if (lastDot >= 0) decSep = ".";
|
|
66
|
+
else if (lastComma >= 0) decSep = ",";
|
|
67
|
+
let major;
|
|
68
|
+
if (decSep) {
|
|
69
|
+
const thousandsSep = decSep === "." ? "," : ".";
|
|
70
|
+
s = s.split(thousandsSep).join("");
|
|
71
|
+
major = Number(s.replace(decSep, "."));
|
|
72
|
+
} else {
|
|
73
|
+
major = Number(s);
|
|
74
|
+
}
|
|
75
|
+
if (Number.isNaN(major)) throw new Error(`Cannot parse money from "${input}"`);
|
|
76
|
+
return _Money.of(neg ? -major : major, currency);
|
|
77
|
+
}
|
|
78
|
+
assertSame(other) {
|
|
79
|
+
if (other.currency !== this.currency) {
|
|
80
|
+
throw new Error(`Currency mismatch: ${this.currency} vs ${other.currency}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
add(other) {
|
|
84
|
+
this.assertSame(other);
|
|
85
|
+
return new _Money(this.amount + other.amount, this.currency);
|
|
86
|
+
}
|
|
87
|
+
subtract(other) {
|
|
88
|
+
this.assertSame(other);
|
|
89
|
+
return new _Money(this.amount - other.amount, this.currency);
|
|
90
|
+
}
|
|
91
|
+
/** Multiply by a scalar (e.g. quantity or tax rate), rounding to whole minor units. */
|
|
92
|
+
multiply(factor) {
|
|
93
|
+
return new _Money(roundHalfUp(this.amount * factor), this.currency);
|
|
94
|
+
}
|
|
95
|
+
/** Divide by a scalar, rounding to whole minor units. */
|
|
96
|
+
divide(divisor) {
|
|
97
|
+
if (divisor === 0) throw new Error("Division by zero");
|
|
98
|
+
return new _Money(roundHalfUp(this.amount / divisor), this.currency);
|
|
99
|
+
}
|
|
100
|
+
negate() {
|
|
101
|
+
return new _Money(-this.amount, this.currency);
|
|
102
|
+
}
|
|
103
|
+
abs() {
|
|
104
|
+
return new _Money(Math.abs(this.amount), this.currency);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Split into parts by integer ratios, distributing leftover minor units to the
|
|
108
|
+
* earliest parts so the sum always equals the original (no cent lost/created).
|
|
109
|
+
* `money(10,"USD").allocate([1,1,1])` → 3.34, 3.33, 3.33.
|
|
110
|
+
*/
|
|
111
|
+
allocate(ratios) {
|
|
112
|
+
if (ratios.length === 0) throw new Error("allocate() needs at least one ratio");
|
|
113
|
+
if (ratios.some((r) => r < 0)) throw new Error("allocate() ratios must be non-negative");
|
|
114
|
+
const total = ratios.reduce((a, b) => a + b, 0);
|
|
115
|
+
if (total === 0) throw new Error("allocate() ratios must not all be zero");
|
|
116
|
+
const shares = ratios.map((r) => Math.floor(this.amount * r / total));
|
|
117
|
+
let remainder = this.amount - shares.reduce((a, b) => a + b, 0);
|
|
118
|
+
const order = ratios.map((r, i) => ({ r, i })).sort((a, b) => b.r - a.r);
|
|
119
|
+
let k = 0;
|
|
120
|
+
while (remainder > 0) {
|
|
121
|
+
shares[order[k % order.length].i] += 1;
|
|
122
|
+
remainder--;
|
|
123
|
+
k++;
|
|
124
|
+
}
|
|
125
|
+
return shares.map((s) => new _Money(s, this.currency));
|
|
126
|
+
}
|
|
127
|
+
/** Split evenly into `n` parts, distributing the remainder fairly. */
|
|
128
|
+
split(n) {
|
|
129
|
+
if (!Number.isInteger(n) || n <= 0) throw new Error("split() needs a positive integer");
|
|
130
|
+
return this.allocate(new Array(n).fill(1));
|
|
131
|
+
}
|
|
132
|
+
equals(other) {
|
|
133
|
+
return this.currency === other.currency && this.amount === other.amount;
|
|
134
|
+
}
|
|
135
|
+
greaterThan(other) {
|
|
136
|
+
this.assertSame(other);
|
|
137
|
+
return this.amount > other.amount;
|
|
138
|
+
}
|
|
139
|
+
lessThan(other) {
|
|
140
|
+
this.assertSame(other);
|
|
141
|
+
return this.amount < other.amount;
|
|
142
|
+
}
|
|
143
|
+
greaterThanOrEqual(other) {
|
|
144
|
+
this.assertSame(other);
|
|
145
|
+
return this.amount >= other.amount;
|
|
146
|
+
}
|
|
147
|
+
lessThanOrEqual(other) {
|
|
148
|
+
this.assertSame(other);
|
|
149
|
+
return this.amount <= other.amount;
|
|
150
|
+
}
|
|
151
|
+
isZero() {
|
|
152
|
+
return this.amount === 0;
|
|
153
|
+
}
|
|
154
|
+
isPositive() {
|
|
155
|
+
return this.amount > 0;
|
|
156
|
+
}
|
|
157
|
+
isNegative() {
|
|
158
|
+
return this.amount < 0;
|
|
159
|
+
}
|
|
160
|
+
/** The value in major units as a number (e.g. 19.99). Beware float for display — prefer format(). */
|
|
161
|
+
toMajor() {
|
|
162
|
+
return this.amount / factorFor(this.currency);
|
|
163
|
+
}
|
|
164
|
+
/** The raw integer minor-unit amount. */
|
|
165
|
+
toMinor() {
|
|
166
|
+
return this.amount;
|
|
167
|
+
}
|
|
168
|
+
/** Localized currency string, e.g. "$1,234.56" / "€1.234,56". */
|
|
169
|
+
format(locale, options) {
|
|
170
|
+
const decimals = decimalsFor(this.currency);
|
|
171
|
+
try {
|
|
172
|
+
return new Intl.NumberFormat(locale, {
|
|
173
|
+
style: "currency",
|
|
174
|
+
currency: this.currency,
|
|
175
|
+
minimumFractionDigits: decimals,
|
|
176
|
+
maximumFractionDigits: decimals,
|
|
177
|
+
...options
|
|
178
|
+
}).format(this.toMajor());
|
|
179
|
+
} catch {
|
|
180
|
+
return `${this.toMajor().toFixed(decimals)} ${this.currency}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
toString() {
|
|
184
|
+
return this.format();
|
|
185
|
+
}
|
|
186
|
+
toJSON() {
|
|
187
|
+
return { amount: this.amount, currency: this.currency };
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function money(major, currency) {
|
|
191
|
+
return Money.of(major, currency);
|
|
192
|
+
}
|
|
193
|
+
function sumMoney(items, currency) {
|
|
194
|
+
if (items.length === 0) {
|
|
195
|
+
if (!currency) throw new Error("sumMoney() of an empty list needs a currency");
|
|
196
|
+
return Money.zero(currency);
|
|
197
|
+
}
|
|
198
|
+
return items.reduce((acc, m) => acc.add(m));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { Money, decimalsFor, money, sumMoney };
|
|
202
|
+
//# sourceMappingURL=index.js.map
|
|
203
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAmBA,IAAM,SAAA,GAAoC;AAAA,EACxC,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAC7F,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK,CAAA;AAAA,EAAG,GAAA,EAAK;AACvD,CAAA;AAGO,SAAS,YAAY,QAAA,EAA0B;AACpD,EAAA,OAAO,SAAA,CAAU,QAAA,CAAS,WAAA,EAAa,CAAA,IAAK,CAAA;AAC9C;AAEA,SAAS,UAAU,QAAA,EAA0B;AAC3C,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,WAAA,CAAY,QAAQ,CAAC,CAAA;AAC3C;AAGA,SAAS,YAAY,CAAA,EAAmB;AACtC,EAAA,OAAO,CAAA,GAAI,CAAA,GAAI,CAAC,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AAC/C;AAEO,IAAM,KAAA,GAAN,MAAM,MAAA,CAAM;AAAA,EAMT,WAAA,CAAY,OAAe,QAAA,EAAkB;AACnD,IAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAI,SAAA,CAAU,CAAA,2DAAA,EAA8D,KAAK,CAAA,CAAE,CAAA;AAAA,IAC3F;AACA,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA;AACd,IAAA,IAAA,CAAK,QAAA,GAAW,SAAS,WAAA,EAAY;AAAA,EACvC;AAAA;AAAA,EAGA,OAAO,EAAA,CAAG,KAAA,EAAe,QAAA,EAAyB;AAChD,IAAA,OAAO,IAAI,OAAM,WAAA,CAAY,KAAA,GAAQ,UAAU,QAAQ,CAAC,GAAG,QAAQ,CAAA;AAAA,EACrE;AAAA;AAAA,EAGA,OAAO,SAAA,CAAU,KAAA,EAAe,QAAA,EAAyB;AACvD,IAAA,OAAO,IAAI,MAAA,CAAM,KAAA,EAAO,QAAQ,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,OAAO,KAAK,QAAA,EAAyB;AACnC,IAAA,OAAO,IAAI,MAAA,CAAM,CAAA,EAAG,QAAQ,CAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,KAAA,CAAM,KAAA,EAAe,QAAA,EAAyB;AACnD,IAAA,MAAM,UAAU,KAAA,CAAM,OAAA,CAAQ,WAAA,EAAa,EAAE,EAAE,IAAA,EAAK;AACpD,IAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,KAAK,CAAA,CAAA,CAAG,CAAA;AAClE,IAAA,MAAM,MAAM,IAAA,CAAK,IAAA,CAAK,OAAO,CAAA,IAAK,KAAA,CAAM,KAAK,OAAO,CAAA;AACpD,IAAA,IAAI,CAAA,GAAI,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAA;AAEhC,IAAA,MAAM,OAAA,GAAU,CAAA,CAAE,WAAA,CAAY,GAAG,CAAA;AACjC,IAAA,MAAM,SAAA,GAAY,CAAA,CAAE,WAAA,CAAY,GAAG,CAAA;AACnC,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,WAAW,CAAA,IAAK,SAAA,IAAa,GAAG,MAAA,GAAS,OAAA,GAAU,YAAY,GAAA,GAAM,GAAA;AAAA,SAAA,IAChE,OAAA,IAAW,GAAG,MAAA,GAAS,GAAA;AAAA,SAAA,IACvB,SAAA,IAAa,GAAG,MAAA,GAAS,GAAA;AAClC,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,MAAM,YAAA,GAAe,MAAA,KAAW,GAAA,GAAM,GAAA,GAAM,GAAA;AAC5C,MAAA,CAAA,GAAI,CAAA,CAAE,KAAA,CAAM,YAAY,CAAA,CAAE,KAAK,EAAE,CAAA;AACjC,MAAA,KAAA,GAAQ,MAAA,CAAO,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,CAAC,CAAA;AAAA,IACvC,CAAA,MAAO;AACL,MAAA,KAAA,GAAQ,OAAO,CAAC,CAAA;AAAA,IAClB;AACA,IAAA,IAAI,MAAA,CAAO,MAAM,KAAK,CAAA,QAAS,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,KAAK,CAAA,CAAA,CAAG,CAAA;AAC7E,IAAA,OAAO,OAAM,EAAA,CAAG,GAAA,GAAM,CAAC,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAAA,EAChD;AAAA,EAEQ,WAAW,KAAA,EAAoB;AACrC,IAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,CAAK,QAAA,EAAU;AACpC,MAAA,MAAM,IAAI,MAAM,CAAA,mBAAA,EAAsB,IAAA,CAAK,QAAQ,CAAA,IAAA,EAAO,KAAA,CAAM,QAAQ,CAAA,CAAE,CAAA;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,IAAI,KAAA,EAAqB;AACvB,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,OAAO,IAAI,MAAA,CAAM,IAAA,CAAK,SAAS,KAAA,CAAM,MAAA,EAAQ,KAAK,QAAQ,CAAA;AAAA,EAC5D;AAAA,EAEA,SAAS,KAAA,EAAqB;AAC5B,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,OAAO,IAAI,MAAA,CAAM,IAAA,CAAK,SAAS,KAAA,CAAM,MAAA,EAAQ,KAAK,QAAQ,CAAA;AAAA,EAC5D;AAAA;AAAA,EAGA,SAAS,MAAA,EAAuB;AAC9B,IAAA,OAAO,IAAI,OAAM,WAAA,CAAY,IAAA,CAAK,SAAS,MAAM,CAAA,EAAG,KAAK,QAAQ,CAAA;AAAA,EACnE;AAAA;AAAA,EAGA,OAAO,OAAA,EAAwB;AAC7B,IAAA,IAAI,OAAA,KAAY,CAAA,EAAG,MAAM,IAAI,MAAM,kBAAkB,CAAA;AACrD,IAAA,OAAO,IAAI,OAAM,WAAA,CAAY,IAAA,CAAK,SAAS,OAAO,CAAA,EAAG,KAAK,QAAQ,CAAA;AAAA,EACpE;AAAA,EAEA,MAAA,GAAgB;AACd,IAAA,OAAO,IAAI,MAAA,CAAM,CAAC,IAAA,CAAK,MAAA,EAAQ,KAAK,QAAQ,CAAA;AAAA,EAC9C;AAAA,EAEA,GAAA,GAAa;AACX,IAAA,OAAO,IAAI,OAAM,IAAA,CAAK,GAAA,CAAI,KAAK,MAAM,CAAA,EAAG,KAAK,QAAQ,CAAA;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,MAAA,EAA2B;AAClC,IAAA,IAAI,OAAO,MAAA,KAAW,CAAA,EAAG,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAC9E,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,wCAAwC,CAAA;AACvF,IAAA,MAAM,KAAA,GAAQ,OAAO,MAAA,CAAO,CAAC,GAAG,CAAA,KAAM,CAAA,GAAI,GAAG,CAAC,CAAA;AAC9C,IAAA,IAAI,KAAA,KAAU,CAAA,EAAG,MAAM,IAAI,MAAM,wCAAwC,CAAA;AACzE,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,IAAA,CAAK,KAAA,CAAO,IAAA,CAAK,MAAA,GAAS,CAAA,GAAK,KAAK,CAAC,CAAA;AACtE,IAAA,IAAI,SAAA,GAAY,IAAA,CAAK,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAA,EAAG,CAAC,CAAA;AAE9D,IAAA,MAAM,QAAQ,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,EAAG,CAAA,MAAO,EAAE,CAAA,EAAG,CAAA,EAAE,CAAE,CAAA,CAAE,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,CAAA,GAAI,EAAE,CAAC,CAAA;AACvE,IAAA,IAAI,CAAA,GAAI,CAAA;AACR,IAAA,OAAO,YAAY,CAAA,EAAG;AACpB,MAAA,MAAA,CAAO,MAAM,CAAA,GAAI,KAAA,CAAM,MAAM,CAAA,CAAG,CAAC,CAAA,IAAM,CAAA;AACvC,MAAA,SAAA,EAAA;AACA,MAAA,CAAA,EAAA;AAAA,IACF;AACA,IAAA,OAAO,MAAA,CAAO,IAAI,CAAC,CAAA,KAAM,IAAI,MAAA,CAAM,CAAA,EAAG,IAAA,CAAK,QAAQ,CAAC,CAAA;AAAA,EACtD;AAAA;AAAA,EAGA,MAAM,CAAA,EAAoB;AACxB,IAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,CAAC,CAAA,IAAK,KAAK,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,kCAAkC,CAAA;AACtF,IAAA,OAAO,IAAA,CAAK,SAAS,IAAI,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,CAAK,CAAC,CAAC,CAAA;AAAA,EAC3C;AAAA,EAEA,OAAO,KAAA,EAAuB;AAC5B,IAAA,OAAO,KAAK,QAAA,KAAa,KAAA,CAAM,QAAA,IAAY,IAAA,CAAK,WAAW,KAAA,CAAM,MAAA;AAAA,EACnE;AAAA,EACA,YAAY,KAAA,EAAuB;AACjC,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,OAAO,IAAA,CAAK,SAAS,KAAA,CAAM,MAAA;AAAA,EAC7B;AAAA,EACA,SAAS,KAAA,EAAuB;AAC9B,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,OAAO,IAAA,CAAK,SAAS,KAAA,CAAM,MAAA;AAAA,EAC7B;AAAA,EACA,mBAAmB,KAAA,EAAuB;AACxC,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,OAAO,IAAA,CAAK,UAAU,KAAA,CAAM,MAAA;AAAA,EAC9B;AAAA,EACA,gBAAgB,KAAA,EAAuB;AACrC,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,OAAO,IAAA,CAAK,UAAU,KAAA,CAAM,MAAA;AAAA,EAC9B;AAAA,EACA,MAAA,GAAkB;AAChB,IAAA,OAAO,KAAK,MAAA,KAAW,CAAA;AAAA,EACzB;AAAA,EACA,UAAA,GAAsB;AACpB,IAAA,OAAO,KAAK,MAAA,GAAS,CAAA;AAAA,EACvB;AAAA,EACA,UAAA,GAAsB;AACpB,IAAA,OAAO,KAAK,MAAA,GAAS,CAAA;AAAA,EACvB;AAAA;AAAA,EAGA,OAAA,GAAkB;AAChB,IAAA,OAAO,IAAA,CAAK,MAAA,GAAS,SAAA,CAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,OAAA,GAAkB;AAChB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA;AAAA,EAGA,MAAA,CAAO,QAAiB,OAAA,EAA4C;AAClE,IAAA,MAAM,QAAA,GAAW,WAAA,CAAY,IAAA,CAAK,QAAQ,CAAA;AAC1C,IAAA,IAAI;AACF,MAAA,OAAO,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,QACnC,KAAA,EAAO,UAAA;AAAA,QACP,UAAU,IAAA,CAAK,QAAA;AAAA,QACf,qBAAA,EAAuB,QAAA;AAAA,QACvB,qBAAA,EAAuB,QAAA;AAAA,QACvB,GAAG;AAAA,OACJ,CAAA,CAAE,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA;AAAA,IAC1B,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,CAAA,EAAG,KAAK,OAAA,EAAQ,CAAE,QAAQ,QAAQ,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,QAAQ,CAAA,CAAA;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,QAAA,GAAmB;AACjB,IAAA,OAAO,KAAK,MAAA,EAAO;AAAA,EACrB;AAAA,EAEA,MAAA,GAA+C;AAC7C,IAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAQ,QAAA,EAAU,KAAK,QAAA,EAAS;AAAA,EACxD;AACF;AAGO,SAAS,KAAA,CAAM,OAAe,QAAA,EAAyB;AAC5D,EAAA,OAAO,KAAA,CAAM,EAAA,CAAG,KAAA,EAAO,QAAQ,CAAA;AACjC;AAGO,SAAS,QAAA,CAAS,OAAgB,QAAA,EAA0B;AACjE,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,IAAI,CAAC,QAAA,EAAU,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAC7E,IAAA,OAAO,KAAA,CAAM,KAAK,QAAQ,CAAA;AAAA,EAC5B;AACA,EAAA,OAAO,KAAA,CAAM,OAAO,CAAC,GAAA,EAAK,MAAM,GAAA,CAAI,GAAA,CAAI,CAAC,CAAC,CAAA;AAC5C","file":"index.js","sourcesContent":["/**\n * @lacspace/money\n * Money done right — integer minor units (no floating-point cents bugs),\n * currency-safe arithmetic, remainder-preserving allocation, and localized\n * formatting via `Intl`.\n *\n * ```ts\n * import { money } from \"@lacspace/money\";\n *\n * const price = money(19.99, \"USD\"); // 1999 minor units\n * const total = price.multiply(3); // $59.97, exact\n * total.format(); // \"$59.97\"\n * money(10, \"USD\").allocate([1, 1, 1]); // [$3.34, $3.33, $3.33] — no cent lost\n * ```\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\n/** Minor-unit exponents for currencies that aren't the default 2. */\nconst EXPONENTS: Record<string, number> = {\n JPY: 0, KRW: 0, VND: 0, CLP: 0, ISK: 0, HUF: 0, XAF: 0, XOF: 0, XPF: 0, RWF: 0, UGX: 0, GNF: 0,\n BHD: 3, KWD: 3, OMR: 3, TND: 3, IQD: 3, JOD: 3, LYD: 3,\n};\n\n/** How many minor units are in one major unit for a currency (e.g. 100 for USD). */\nexport function decimalsFor(currency: string): number {\n return EXPONENTS[currency.toUpperCase()] ?? 2;\n}\n\nfunction factorFor(currency: string): number {\n return Math.pow(10, decimalsFor(currency));\n}\n\n/** Round half away from zero — the intuitive rule for money. */\nfunction roundHalfUp(n: number): number {\n return n < 0 ? -Math.round(-n) : Math.round(n);\n}\n\nexport class Money {\n /** Integer amount in minor units (e.g. cents). */\n readonly amount: number;\n /** ISO 4217 code, upper-cased. */\n readonly currency: string;\n\n private constructor(minor: number, currency: string) {\n if (!Number.isInteger(minor)) {\n throw new TypeError(`Money amount must be an integer number of minor units, got ${minor}`);\n }\n this.amount = minor;\n this.currency = currency.toUpperCase();\n }\n\n /** From major units: `Money.of(19.99, \"USD\")` → 1999 minor units. */\n static of(major: number, currency: string): Money {\n return new Money(roundHalfUp(major * factorFor(currency)), currency);\n }\n\n /** From an integer count of minor units: `Money.fromMinor(1999, \"USD\")`. */\n static fromMinor(minor: number, currency: string): Money {\n return new Money(minor, currency);\n }\n\n /** Zero in a currency. */\n static zero(currency: string): Money {\n return new Money(0, currency);\n }\n\n /**\n * Parse a formatted string like \"$1,234.56\" or \"1.234,56\" (best-effort).\n * Non-digits except the last separator group are stripped.\n */\n static parse(input: string, currency: string): Money {\n const cleaned = input.replace(/[^\\d.,-]/g, \"\").trim();\n if (!cleaned) throw new Error(`Cannot parse money from \"${input}\"`);\n const neg = /^-/.test(cleaned) || /-\\d/.test(cleaned);\n let s = cleaned.replace(/-/g, \"\");\n // Decide the decimal separator: whichever of . or , comes last.\n const lastDot = s.lastIndexOf(\".\");\n const lastComma = s.lastIndexOf(\",\");\n let decSep = \"\";\n if (lastDot >= 0 && lastComma >= 0) decSep = lastDot > lastComma ? \".\" : \",\";\n else if (lastDot >= 0) decSep = \".\";\n else if (lastComma >= 0) decSep = \",\";\n let major: number;\n if (decSep) {\n const thousandsSep = decSep === \".\" ? \",\" : \".\";\n s = s.split(thousandsSep).join(\"\");\n major = Number(s.replace(decSep, \".\"));\n } else {\n major = Number(s);\n }\n if (Number.isNaN(major)) throw new Error(`Cannot parse money from \"${input}\"`);\n return Money.of(neg ? -major : major, currency);\n }\n\n private assertSame(other: Money): void {\n if (other.currency !== this.currency) {\n throw new Error(`Currency mismatch: ${this.currency} vs ${other.currency}`);\n }\n }\n\n add(other: Money): Money {\n this.assertSame(other);\n return new Money(this.amount + other.amount, this.currency);\n }\n\n subtract(other: Money): Money {\n this.assertSame(other);\n return new Money(this.amount - other.amount, this.currency);\n }\n\n /** Multiply by a scalar (e.g. quantity or tax rate), rounding to whole minor units. */\n multiply(factor: number): Money {\n return new Money(roundHalfUp(this.amount * factor), this.currency);\n }\n\n /** Divide by a scalar, rounding to whole minor units. */\n divide(divisor: number): Money {\n if (divisor === 0) throw new Error(\"Division by zero\");\n return new Money(roundHalfUp(this.amount / divisor), this.currency);\n }\n\n negate(): Money {\n return new Money(-this.amount, this.currency);\n }\n\n abs(): Money {\n return new Money(Math.abs(this.amount), this.currency);\n }\n\n /**\n * Split into parts by integer ratios, distributing leftover minor units to the\n * earliest parts so the sum always equals the original (no cent lost/created).\n * `money(10,\"USD\").allocate([1,1,1])` → 3.34, 3.33, 3.33.\n */\n allocate(ratios: number[]): Money[] {\n if (ratios.length === 0) throw new Error(\"allocate() needs at least one ratio\");\n if (ratios.some((r) => r < 0)) throw new Error(\"allocate() ratios must be non-negative\");\n const total = ratios.reduce((a, b) => a + b, 0);\n if (total === 0) throw new Error(\"allocate() ratios must not all be zero\");\n const shares = ratios.map((r) => Math.floor((this.amount * r) / total));\n let remainder = this.amount - shares.reduce((a, b) => a + b, 0);\n // Hand out the remaining units one at a time to the parts with the biggest ratio.\n const order = ratios.map((r, i) => ({ r, i })).sort((a, b) => b.r - a.r);\n let k = 0;\n while (remainder > 0) {\n shares[order[k % order.length]!.i]! += 1;\n remainder--;\n k++;\n }\n return shares.map((s) => new Money(s, this.currency));\n }\n\n /** Split evenly into `n` parts, distributing the remainder fairly. */\n split(n: number): Money[] {\n if (!Number.isInteger(n) || n <= 0) throw new Error(\"split() needs a positive integer\");\n return this.allocate(new Array(n).fill(1));\n }\n\n equals(other: Money): boolean {\n return this.currency === other.currency && this.amount === other.amount;\n }\n greaterThan(other: Money): boolean {\n this.assertSame(other);\n return this.amount > other.amount;\n }\n lessThan(other: Money): boolean {\n this.assertSame(other);\n return this.amount < other.amount;\n }\n greaterThanOrEqual(other: Money): boolean {\n this.assertSame(other);\n return this.amount >= other.amount;\n }\n lessThanOrEqual(other: Money): boolean {\n this.assertSame(other);\n return this.amount <= other.amount;\n }\n isZero(): boolean {\n return this.amount === 0;\n }\n isPositive(): boolean {\n return this.amount > 0;\n }\n isNegative(): boolean {\n return this.amount < 0;\n }\n\n /** The value in major units as a number (e.g. 19.99). Beware float for display — prefer format(). */\n toMajor(): number {\n return this.amount / factorFor(this.currency);\n }\n\n /** The raw integer minor-unit amount. */\n toMinor(): number {\n return this.amount;\n }\n\n /** Localized currency string, e.g. \"$1,234.56\" / \"€1.234,56\". */\n format(locale?: string, options?: Intl.NumberFormatOptions): string {\n const decimals = decimalsFor(this.currency);\n try {\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: this.currency,\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n ...options,\n }).format(this.toMajor());\n } catch {\n // Unknown currency to Intl — fall back to a plain number + code.\n return `${this.toMajor().toFixed(decimals)} ${this.currency}`;\n }\n }\n\n toString(): string {\n return this.format();\n }\n\n toJSON(): { amount: number; currency: string } {\n return { amount: this.amount, currency: this.currency };\n }\n}\n\n/** Friendly constructor from major units: `money(19.99, \"USD\")`. */\nexport function money(major: number, currency: string): Money {\n return Money.of(major, currency);\n}\n\n/** Sum a list of Money (all same currency). Empty list requires a currency fallback. */\nexport function sumMoney(items: Money[], currency?: string): Money {\n if (items.length === 0) {\n if (!currency) throw new Error(\"sumMoney() of an empty list needs a currency\");\n return Money.zero(currency);\n }\n return items.reduce((acc, m) => acc.add(m));\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/money",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Money done right — integer minor units (no floating-point cent bugs), currency-safe arithmetic, remainder-preserving allocation/split, and localized formatting via Intl. Zero-dependency, isomorphic.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"money",
|
|
31
|
+
"currency",
|
|
32
|
+
"dinero-alternative",
|
|
33
|
+
"minor-units",
|
|
34
|
+
"cents",
|
|
35
|
+
"decimal",
|
|
36
|
+
"allocate",
|
|
37
|
+
"intl",
|
|
38
|
+
"currency-formatting",
|
|
39
|
+
"finance",
|
|
40
|
+
"price",
|
|
41
|
+
"typescript",
|
|
42
|
+
"zero-dependency",
|
|
43
|
+
"isomorphic"
|
|
44
|
+
],
|
|
45
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
46
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
47
|
+
"homepage": "https://lacspace.com/packages",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
51
|
+
"directory": "money"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/lacspace/npm-packages/issues"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|