@ducatjs/money 0.0.0 → 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.
Files changed (285) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +51 -0
  3. package/README.md +617 -3
  4. package/assets/CREDITS.md +12 -0
  5. package/data/adyen-fx.json +19 -0
  6. package/data/adyen.json +171 -0
  7. package/data/braintree.json +223 -0
  8. package/data/cash-rounding.json +399 -0
  9. package/data/checkout.json +230 -0
  10. package/data/iso4217-list-one.xml +1956 -0
  11. package/data/klarna.json +95 -0
  12. package/data/paypal.json +65 -0
  13. package/data/stripe-fx.json +17 -0
  14. package/data/stripe.json +285 -0
  15. package/dist/adyen-fx.d.ts +25 -0
  16. package/dist/adyen-fx.d.ts.map +1 -0
  17. package/dist/adyen-fx.js +122 -0
  18. package/dist/adyen-fx.js.map +1 -0
  19. package/dist/adyen.d.ts +25 -0
  20. package/dist/adyen.d.ts.map +1 -0
  21. package/dist/adyen.js +34 -0
  22. package/dist/adyen.js.map +1 -0
  23. package/dist/allocate.d.ts +72 -0
  24. package/dist/allocate.d.ts.map +1 -0
  25. package/dist/allocate.js +314 -0
  26. package/dist/allocate.js.map +1 -0
  27. package/dist/bag.d.ts +58 -0
  28. package/dist/bag.d.ts.map +1 -0
  29. package/dist/bag.js +122 -0
  30. package/dist/bag.js.map +1 -0
  31. package/dist/braintree.d.ts +34 -0
  32. package/dist/braintree.d.ts.map +1 -0
  33. package/dist/braintree.js +45 -0
  34. package/dist/braintree.js.map +1 -0
  35. package/dist/cash.d.ts +35 -0
  36. package/dist/cash.d.ts.map +1 -0
  37. package/dist/cash.js +86 -0
  38. package/dist/cash.js.map +1 -0
  39. package/dist/checkout.d.ts +42 -0
  40. package/dist/checkout.d.ts.map +1 -0
  41. package/dist/checkout.js +52 -0
  42. package/dist/checkout.js.map +1 -0
  43. package/dist/convert.d.ts +51 -0
  44. package/dist/convert.d.ts.map +1 -0
  45. package/dist/convert.js +55 -0
  46. package/dist/convert.js.map +1 -0
  47. package/dist/currency.d.ts +22 -0
  48. package/dist/currency.d.ts.map +1 -0
  49. package/dist/currency.js +38 -0
  50. package/dist/currency.js.map +1 -0
  51. package/dist/decimal.d.ts +52 -0
  52. package/dist/decimal.d.ts.map +1 -0
  53. package/dist/decimal.js +251 -0
  54. package/dist/decimal.js.map +1 -0
  55. package/dist/ecb.d.ts +17 -0
  56. package/dist/ecb.d.ts.map +1 -0
  57. package/dist/ecb.js +147 -0
  58. package/dist/ecb.js.map +1 -0
  59. package/dist/exact.d.ts +62 -0
  60. package/dist/exact.d.ts.map +1 -0
  61. package/dist/exact.js +147 -0
  62. package/dist/exact.js.map +1 -0
  63. package/dist/fetch.d.ts +58 -0
  64. package/dist/fetch.d.ts.map +1 -0
  65. package/dist/fetch.js +79 -0
  66. package/dist/fetch.js.map +1 -0
  67. package/dist/fixed.d.ts +121 -0
  68. package/dist/fixed.d.ts.map +1 -0
  69. package/dist/fixed.js +420 -0
  70. package/dist/fixed.js.map +1 -0
  71. package/dist/fn.d.ts +27 -0
  72. package/dist/fn.d.ts.map +1 -0
  73. package/dist/fn.js +30 -0
  74. package/dist/fn.js.map +1 -0
  75. package/dist/format.d.ts +13 -0
  76. package/dist/format.d.ts.map +1 -0
  77. package/dist/format.js +90 -0
  78. package/dist/format.js.map +1 -0
  79. package/dist/generated/adyen.d.ts +3 -0
  80. package/dist/generated/adyen.d.ts.map +1 -0
  81. package/dist/generated/adyen.js +144 -0
  82. package/dist/generated/adyen.js.map +1 -0
  83. package/dist/generated/braintree.d.ts +3 -0
  84. package/dist/generated/braintree.d.ts.map +1 -0
  85. package/dist/generated/braintree.js +140 -0
  86. package/dist/generated/braintree.js.map +1 -0
  87. package/dist/generated/cash.d.ts +12 -0
  88. package/dist/generated/cash.d.ts.map +1 -0
  89. package/dist/generated/cash.js +364 -0
  90. package/dist/generated/cash.js.map +1 -0
  91. package/dist/generated/checkout.d.ts +3 -0
  92. package/dist/generated/checkout.d.ts.map +1 -0
  93. package/dist/generated/checkout.js +151 -0
  94. package/dist/generated/checkout.js.map +1 -0
  95. package/dist/generated/iso4217.d.ts +174 -0
  96. package/dist/generated/iso4217.d.ts.map +1 -0
  97. package/dist/generated/iso4217.js +175 -0
  98. package/dist/generated/iso4217.js.map +1 -0
  99. package/dist/generated/kernel.d.ts +17 -0
  100. package/dist/generated/kernel.d.ts.map +1 -0
  101. package/dist/generated/kernel.js +208 -0
  102. package/dist/generated/kernel.js.map +1 -0
  103. package/dist/generated/klarna.d.ts +3 -0
  104. package/dist/generated/klarna.d.ts.map +1 -0
  105. package/dist/generated/klarna.js +21 -0
  106. package/dist/generated/klarna.js.map +1 -0
  107. package/dist/generated/paypal.d.ts +3 -0
  108. package/dist/generated/paypal.d.ts.map +1 -0
  109. package/dist/generated/paypal.js +31 -0
  110. package/dist/generated/paypal.js.map +1 -0
  111. package/dist/generated/stripe.d.ts +8 -0
  112. package/dist/generated/stripe.d.ts.map +1 -0
  113. package/dist/generated/stripe.js +150 -0
  114. package/dist/generated/stripe.js.map +1 -0
  115. package/dist/index.d.ts +11 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +11 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/klarna.d.ts +31 -0
  120. package/dist/klarna.d.ts.map +1 -0
  121. package/dist/klarna.js +38 -0
  122. package/dist/klarna.js.map +1 -0
  123. package/dist/money.d.ts +218 -0
  124. package/dist/money.d.ts.map +1 -0
  125. package/dist/money.js +577 -0
  126. package/dist/money.js.map +1 -0
  127. package/dist/parse.d.ts +11 -0
  128. package/dist/parse.d.ts.map +1 -0
  129. package/dist/parse.js +132 -0
  130. package/dist/parse.js.map +1 -0
  131. package/dist/paypal.d.ts +41 -0
  132. package/dist/paypal.d.ts.map +1 -0
  133. package/dist/paypal.js +58 -0
  134. package/dist/paypal.js.map +1 -0
  135. package/dist/plugins/adyen.d.ts +15 -0
  136. package/dist/plugins/adyen.d.ts.map +1 -0
  137. package/dist/plugins/adyen.js +10 -0
  138. package/dist/plugins/adyen.js.map +1 -0
  139. package/dist/plugins/all.d.ts +22 -0
  140. package/dist/plugins/all.d.ts.map +1 -0
  141. package/dist/plugins/all.js +23 -0
  142. package/dist/plugins/all.js.map +1 -0
  143. package/dist/plugins/allocate.d.ts +20 -0
  144. package/dist/plugins/allocate.d.ts.map +1 -0
  145. package/dist/plugins/allocate.js +26 -0
  146. package/dist/plugins/allocate.js.map +1 -0
  147. package/dist/plugins/braintree.d.ts +12 -0
  148. package/dist/plugins/braintree.d.ts.map +1 -0
  149. package/dist/plugins/braintree.js +12 -0
  150. package/dist/plugins/braintree.js.map +1 -0
  151. package/dist/plugins/cash.d.ts +19 -0
  152. package/dist/plugins/cash.d.ts.map +1 -0
  153. package/dist/plugins/cash.js +16 -0
  154. package/dist/plugins/cash.js.map +1 -0
  155. package/dist/plugins/checkout.d.ts +21 -0
  156. package/dist/plugins/checkout.d.ts.map +1 -0
  157. package/dist/plugins/checkout.js +16 -0
  158. package/dist/plugins/checkout.js.map +1 -0
  159. package/dist/plugins/convert.d.ts +20 -0
  160. package/dist/plugins/convert.d.ts.map +1 -0
  161. package/dist/plugins/convert.js +13 -0
  162. package/dist/plugins/convert.js.map +1 -0
  163. package/dist/plugins/exact.d.ts +15 -0
  164. package/dist/plugins/exact.d.ts.map +1 -0
  165. package/dist/plugins/exact.js +13 -0
  166. package/dist/plugins/exact.js.map +1 -0
  167. package/dist/plugins/fixed.d.ts +17 -0
  168. package/dist/plugins/fixed.d.ts.map +1 -0
  169. package/dist/plugins/fixed.js +13 -0
  170. package/dist/plugins/fixed.js.map +1 -0
  171. package/dist/plugins/format.d.ts +9 -0
  172. package/dist/plugins/format.d.ts.map +1 -0
  173. package/dist/plugins/format.js +16 -0
  174. package/dist/plugins/format.js.map +1 -0
  175. package/dist/plugins/klarna.d.ts +10 -0
  176. package/dist/plugins/klarna.d.ts.map +1 -0
  177. package/dist/plugins/klarna.js +10 -0
  178. package/dist/plugins/klarna.js.map +1 -0
  179. package/dist/plugins/paypal.d.ts +14 -0
  180. package/dist/plugins/paypal.d.ts.map +1 -0
  181. package/dist/plugins/paypal.js +16 -0
  182. package/dist/plugins/paypal.js.map +1 -0
  183. package/dist/plugins/prisma.d.ts +8 -0
  184. package/dist/plugins/prisma.d.ts.map +1 -0
  185. package/dist/plugins/prisma.js +13 -0
  186. package/dist/plugins/prisma.js.map +1 -0
  187. package/dist/plugins/stripe.d.ts +14 -0
  188. package/dist/plugins/stripe.d.ts.map +1 -0
  189. package/dist/plugins/stripe.js +16 -0
  190. package/dist/plugins/stripe.js.map +1 -0
  191. package/dist/plugins/tax.d.ts +21 -0
  192. package/dist/plugins/tax.d.ts.map +1 -0
  193. package/dist/plugins/tax.js +18 -0
  194. package/dist/plugins/tax.js.map +1 -0
  195. package/dist/prisma.d.ts +66 -0
  196. package/dist/prisma.d.ts.map +1 -0
  197. package/dist/prisma.js +66 -0
  198. package/dist/prisma.js.map +1 -0
  199. package/dist/processor.d.ts +28 -0
  200. package/dist/processor.d.ts.map +1 -0
  201. package/dist/processor.js +118 -0
  202. package/dist/processor.js.map +1 -0
  203. package/dist/provider.d.ts +89 -0
  204. package/dist/provider.d.ts.map +1 -0
  205. package/dist/provider.js +240 -0
  206. package/dist/provider.js.map +1 -0
  207. package/dist/rate.d.ts +119 -0
  208. package/dist/rate.d.ts.map +1 -0
  209. package/dist/rate.js +271 -0
  210. package/dist/rate.js.map +1 -0
  211. package/dist/rounding-error.d.ts +9 -0
  212. package/dist/rounding-error.d.ts.map +1 -0
  213. package/dist/rounding-error.js +9 -0
  214. package/dist/rounding-error.js.map +1 -0
  215. package/dist/rounding.d.ts +50 -0
  216. package/dist/rounding.d.ts.map +1 -0
  217. package/dist/rounding.js +87 -0
  218. package/dist/rounding.js.map +1 -0
  219. package/dist/stripe-fx.d.ts +22 -0
  220. package/dist/stripe-fx.d.ts.map +1 -0
  221. package/dist/stripe-fx.js +30 -0
  222. package/dist/stripe-fx.js.map +1 -0
  223. package/dist/stripe.d.ts +36 -0
  224. package/dist/stripe.d.ts.map +1 -0
  225. package/dist/stripe.js +39 -0
  226. package/dist/stripe.js.map +1 -0
  227. package/dist/tax.d.ts +29 -0
  228. package/dist/tax.d.ts.map +1 -0
  229. package/dist/tax.js +39 -0
  230. package/dist/tax.js.map +1 -0
  231. package/package.json +169 -5
  232. package/src/adyen-fx.ts +154 -0
  233. package/src/adyen.ts +46 -0
  234. package/src/allocate.ts +393 -0
  235. package/src/bag.ts +170 -0
  236. package/src/braintree.ts +71 -0
  237. package/src/cash.ts +115 -0
  238. package/src/checkout.ts +77 -0
  239. package/src/convert.ts +159 -0
  240. package/src/currency.ts +61 -0
  241. package/src/decimal.ts +283 -0
  242. package/src/ecb.ts +162 -0
  243. package/src/exact.ts +182 -0
  244. package/src/fetch.ts +127 -0
  245. package/src/fixed.ts +558 -0
  246. package/src/fn.ts +80 -0
  247. package/src/format.ts +106 -0
  248. package/src/generated/adyen.ts +144 -0
  249. package/src/generated/braintree.ts +140 -0
  250. package/src/generated/cash.ts +375 -0
  251. package/src/generated/checkout.ts +151 -0
  252. package/src/generated/iso4217.ts +179 -0
  253. package/src/generated/kernel.ts +230 -0
  254. package/src/generated/klarna.ts +21 -0
  255. package/src/generated/paypal.ts +31 -0
  256. package/src/generated/stripe.ts +150 -0
  257. package/src/index.ts +37 -0
  258. package/src/klarna.ts +54 -0
  259. package/src/money.ts +781 -0
  260. package/src/parse.ts +144 -0
  261. package/src/paypal.ts +93 -0
  262. package/src/plugins/adyen.ts +25 -0
  263. package/src/plugins/all.ts +47 -0
  264. package/src/plugins/allocate.ts +53 -0
  265. package/src/plugins/braintree.ts +23 -0
  266. package/src/plugins/cash.ts +33 -0
  267. package/src/plugins/checkout.ts +32 -0
  268. package/src/plugins/convert.ts +50 -0
  269. package/src/plugins/exact.ts +23 -0
  270. package/src/plugins/fixed.ts +28 -0
  271. package/src/plugins/format.ts +28 -0
  272. package/src/plugins/klarna.ts +21 -0
  273. package/src/plugins/paypal.ts +29 -0
  274. package/src/plugins/prisma.ts +24 -0
  275. package/src/plugins/stripe.ts +29 -0
  276. package/src/plugins/tax.ts +41 -0
  277. package/src/prisma.ts +118 -0
  278. package/src/processor.ts +181 -0
  279. package/src/provider.ts +324 -0
  280. package/src/rate.ts +377 -0
  281. package/src/rounding-error.ts +8 -0
  282. package/src/rounding.ts +122 -0
  283. package/src/stripe-fx.ts +49 -0
  284. package/src/stripe.ts +69 -0
  285. package/src/tax.ts +71 -0
@@ -0,0 +1,251 @@
1
+ // Exact decimal-string handling. No floating point is used on amounts: numbers given as factors are
2
+ // converted through their shortest round-trip decimal representation (String(x)), never multiplied.
3
+ /** The largest minor-unit count held as a number (2^53 - 1); larger values are bigints. */
4
+ const MAX_SAFE_MINOR = Number.MAX_SAFE_INTEGER;
5
+ const MAXN = BigInt(MAX_SAFE_MINOR);
6
+ const POW10N = [];
7
+ /** 10^exponent as a bigint, cached. */
8
+ export function pow10n(exponent) {
9
+ return (POW10N[exponent] ??= 10n ** BigInt(exponent));
10
+ }
11
+ /** 10^e as a number for 0 <= e <= 15 (all exact and safe). */
12
+ export const POW10 = [
13
+ 1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
14
+ ];
15
+ /**
16
+ * A bigint as a number when it is a safe integer, else unchanged: the representation Money keeps.
17
+ */
18
+ export const normalizeMinor = (value) => value <= MAXN && value >= -MAXN ? Number(value) : value;
19
+ /**
20
+ * Parses a plain decimal string ("-12.345", "+7", "0.50") into digits/10^scale. Returns the integer
21
+ * of all digits (number when safe, else bigint) and the count of fraction digits. Hand-rolled (no
22
+ * regex, no BigInt for short inputs): this is the hot path of `money("12.34", ...)`.
23
+ */
24
+ export function parsePlainDecimal(input) {
25
+ const len = input.length;
26
+ let i = 0;
27
+ let neg = false;
28
+ let code = input.charCodeAt(0);
29
+ if (code === 45 /* - */ || code === 43 /* + */) {
30
+ neg = code === 45;
31
+ i = 1;
32
+ }
33
+ let acc = 0;
34
+ let digitsSeen = 0;
35
+ let scale = -1;
36
+ let big = false;
37
+ const start = i;
38
+ for (; i < len; i++) {
39
+ code = input.charCodeAt(i);
40
+ if (code >= 48 && code <= 57) {
41
+ digitsSeen++;
42
+ if (scale >= 0) {
43
+ scale++;
44
+ }
45
+ if (!big) {
46
+ // 15 digits always fit exactly in a double; beyond that use BigInt
47
+ if (digitsSeen > 15) {
48
+ big = true;
49
+ }
50
+ else {
51
+ acc = acc * 10 + (code - 48);
52
+ }
53
+ }
54
+ }
55
+ else if (code === 46 /* . */ && scale < 0 && digitsSeen > 0) {
56
+ scale = 0;
57
+ }
58
+ else {
59
+ return null;
60
+ }
61
+ }
62
+ // "", "-" and "12." are not decimals
63
+ if (digitsSeen === 0 || scale === 0) {
64
+ return null;
65
+ }
66
+ // parseDecimal reports fractions longer than MAX_DECIMAL_SCALE
67
+ if (scale > MAX_DECIMAL_SCALE) {
68
+ return null;
69
+ }
70
+ if (scale < 0) {
71
+ scale = 0;
72
+ }
73
+ if (big) {
74
+ const raw = input.slice(start).replace('.', '');
75
+ const value = BigInt(raw);
76
+ return { digits: normalizeMinor(neg ? -value : value), scale };
77
+ }
78
+ return { digits: neg ? 0 - acc : acc, scale };
79
+ }
80
+ const EXP_RE = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
81
+ /**
82
+ * The largest exponent and number of fraction digits a decimal string may have. Far beyond any real
83
+ * amount or rate, and it keeps input such as "1e999999999" from building enormous powers of ten.
84
+ */
85
+ export const MAX_DECIMAL_SCALE = 1000;
86
+ /** Parses a decimal string, allowing exponent notation ("1e-7", as produced by String(1e-7)). */
87
+ export function parseDecimal(input) {
88
+ const match = EXP_RE.exec(input.trim());
89
+ if (!match) {
90
+ throw new SyntaxError(`Invalid decimal string "${input}"`);
91
+ }
92
+ const [, sign, int, frac = '', exp = '0'] = match;
93
+ const exponent = Number(exp);
94
+ let scale = frac.length - exponent;
95
+ if (!(Math.abs(exponent) <= MAX_DECIMAL_SCALE) || scale > MAX_DECIMAL_SCALE) {
96
+ // The input itself is not quoted: it may be huge.
97
+ throw new RangeError(`Decimal string exponent or fraction digits beyond ${MAX_DECIMAL_SCALE}`);
98
+ }
99
+ let digits = BigInt(int + frac);
100
+ if (scale < 0) {
101
+ digits *= pow10n(-scale);
102
+ scale = 0;
103
+ }
104
+ return { digits: sign === '-' ? -digits : digits, scale };
105
+ }
106
+ function gcd(left, right) {
107
+ if (left < 0n) {
108
+ left = -left;
109
+ }
110
+ // a temporary, not a destructuring swap: `[a, b] = [b, a % b]` builds an array per step until the
111
+ // optimiser removes it, and this loop runs for every ratio that is built
112
+ while (right) {
113
+ const remainder = left % right;
114
+ left = right;
115
+ right = remainder;
116
+ }
117
+ return left;
118
+ }
119
+ /** The value as a number when it is a safe integer, otherwise NaN (a Ratio's `n` and `d`). */
120
+ function safeNumberOrNaN(value) {
121
+ return value <= MAXN && value >= -MAXN ? Number(value) : NaN;
122
+ }
123
+ function makeRatio(num, den) {
124
+ if (den === 0n) {
125
+ throw new RangeError('Division by zero');
126
+ }
127
+ if (den < 0n) {
128
+ num = -num;
129
+ den = -den;
130
+ }
131
+ // an integer (den 1) is already in lowest terms
132
+ if (den !== 1n) {
133
+ const divisor = gcd(num, den);
134
+ if (divisor > 1n) {
135
+ num /= divisor;
136
+ den /= divisor;
137
+ }
138
+ }
139
+ return { num, den, n: safeNumberOrNaN(num), d: safeNumberOrNaN(den) };
140
+ }
141
+ const cache = new Map();
142
+ const INT_RATIOS = [];
143
+ /**
144
+ * Builds an exact ratio. `ratio('1.075')`, `ratio(3, 4)`, `ratio(10n)`, `ratio('1/3')`.
145
+ * Numbers are taken at their shortest decimal representation: `ratio(0.1)` is exactly 1/10.
146
+ * Pre-build ratios you use in hot loops; strings are also cached (up to 1024 entries).
147
+ */
148
+ export function ratio(value, den) {
149
+ if (den !== undefined) {
150
+ // two bigints (divide, percentages, tax): one reduction instead of three
151
+ if (typeof value === 'bigint' && typeof den === 'bigint') {
152
+ return makeRatio(value, den);
153
+ }
154
+ const numeratorRatio = ratio(value);
155
+ const denominatorRatio = ratio(den);
156
+ return makeRatio(numeratorRatio.num * denominatorRatio.den, numeratorRatio.den * denominatorRatio.num);
157
+ }
158
+ if (typeof value === 'object') {
159
+ return value;
160
+ }
161
+ if (typeof value === 'number') {
162
+ if (Number.isSafeInteger(value)) {
163
+ if (value >= 0 && value < 128) {
164
+ return (INT_RATIOS[value] ??= makeRatio(BigInt(value), 1n));
165
+ }
166
+ return makeRatio(BigInt(value), 1n);
167
+ }
168
+ if (!Number.isFinite(value)) {
169
+ throw new RangeError(`Invalid factor ${value}`);
170
+ }
171
+ value = String(value);
172
+ }
173
+ if (typeof value === 'bigint') {
174
+ return makeRatio(value, 1n);
175
+ }
176
+ let cached = cache.get(value);
177
+ if (cached === undefined) {
178
+ const slash = value.indexOf('/');
179
+ if (slash > 0) {
180
+ cached = ratio(value.slice(0, slash).trim(), value.slice(slash + 1).trim());
181
+ }
182
+ else {
183
+ const { digits, scale } = parseDecimal(value);
184
+ cached = makeRatio(digits, pow10n(scale));
185
+ }
186
+ if (cache.size >= 1024) {
187
+ cache.clear();
188
+ }
189
+ cache.set(value, cached);
190
+ }
191
+ return cached;
192
+ }
193
+ /** num/den -> exact decimal string if the expansion terminates, else null. */
194
+ export function ratioToDecimal(value) {
195
+ let remainingDenominator = value.den;
196
+ let twos = 0;
197
+ let fives = 0;
198
+ while (remainingDenominator % 2n === 0n) {
199
+ remainingDenominator /= 2n;
200
+ twos++;
201
+ }
202
+ while (remainingDenominator % 5n === 0n) {
203
+ remainingDenominator /= 5n;
204
+ fives++;
205
+ }
206
+ if (remainingDenominator !== 1n) {
207
+ return null;
208
+ }
209
+ const scale = Math.max(twos, fives);
210
+ return formatScaled(value.num * (pow10n(scale) / value.den), scale);
211
+ }
212
+ // Zero padding for the fraction digits, built once: indexing it is faster than padStart.
213
+ const ZPAD = Array.from({ length: 16 }, (_, length) => '0'.repeat(length));
214
+ /** Formats an integer at `scale` as a plain decimal string ("-0.05"). */
215
+ export function formatScaled(minor, scale) {
216
+ if (typeof minor === 'number' && scale <= 15) {
217
+ if (scale === 0) {
218
+ return String(minor);
219
+ }
220
+ // integer/fraction split with exact integer arithmetic (the float quotient is corrected)
221
+ const magnitude = minor < 0 ? 0 - minor : minor;
222
+ const unit = POW10[scale];
223
+ if (magnitude + unit <= MAX_SAFE_MINOR) {
224
+ let whole = Math.floor(magnitude / unit);
225
+ let fraction = magnitude - whole * unit;
226
+ if (fraction < 0) {
227
+ whole -= 1;
228
+ fraction += unit;
229
+ }
230
+ else if (fraction >= unit) {
231
+ whole += 1;
232
+ fraction -= unit;
233
+ }
234
+ let fractionDigits = String(fraction);
235
+ if (fractionDigits.length < scale) {
236
+ fractionDigits = ZPAD[scale - fractionDigits.length] + fractionDigits;
237
+ }
238
+ return (minor < 0 ? '-' : '') + whole + '.' + fractionDigits;
239
+ }
240
+ }
241
+ const neg = minor < 0;
242
+ let digits = typeof minor === 'number' ? String(neg ? 0 - minor : minor) : (neg ? -minor : minor).toString();
243
+ if (scale > 0) {
244
+ if (digits.length <= scale) {
245
+ digits = '0'.repeat(scale - digits.length + 1) + digits;
246
+ }
247
+ digits = digits.slice(0, digits.length - scale) + '.' + digits.slice(digits.length - scale);
248
+ }
249
+ return neg ? '-' + digits : digits;
250
+ }
251
+ //# sourceMappingURL=decimal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.js","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"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"}
package/dist/ecb.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type RateTable } from './provider.js';
2
+ /**
3
+ * Parses eurofxref-daily.xml, eurofxref-hist-90d.xml or eurofxref-hist.xml into one EUR-based table
4
+ * per day, newest first. Each rate is EUR → currency, with `source: 'ECB'`, `type: 'reference'` and
5
+ * `asOf` the publication day. A day without any rates is left out, so a lookback goes on to the day
6
+ * before. A rate entry that can't be read throws SyntaxError.
7
+ *
8
+ * const [latest] = parseEcbXml(await readFile('eurofxref-daily.xml', 'utf8'));
9
+ */
10
+ export declare function parseEcbXml(xml: string): RateTable[];
11
+ /**
12
+ * Parses eurofxref.csv or eurofxref-hist.csv (from the ECB's zips) the way parseEcbXml does.
13
+ * 'N/A' and blank cells (currencies not quoted that day) are skipped, and a day with no rates at
14
+ * all is left out, so a lookback goes on to the day before.
15
+ */
16
+ export declare function parseEcbCsv(csv: string): RateTable[];
17
+ //# sourceMappingURL=ecb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ecb.d.ts","sourceRoot":"","sources":["../src/ecb.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAuE1D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CAgBpD;AAiCD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CA0BpD"}
package/dist/ecb.js ADDED
@@ -0,0 +1,147 @@
1
+ // Offline parsers for the ECB euro foreign exchange reference rates (eurofxref). No I/O: fetch or
2
+ // read the files yourself, or use ducat/convert/fetch. Rates are kept exactly as published.
3
+ import { rateTable } from './provider.js';
4
+ const MONTHS = [
5
+ 'january',
6
+ 'february',
7
+ 'march',
8
+ 'april',
9
+ 'may',
10
+ 'june',
11
+ 'july',
12
+ 'august',
13
+ 'september',
14
+ 'october',
15
+ 'november',
16
+ 'december',
17
+ ];
18
+ const DAY_CUBE = /<Cube\s+time=["'](\d{4}-\d\d-\d\d)["']\s*>([\s\S]*?)<\/Cube>/g;
19
+ // One <Cube .../> element, and its attributes in any order and with any quoting or spacing.
20
+ const INNER_CUBE = /<Cube\b([^>]*?)\/?>/g;
21
+ const ATTRIBUTE = /([A-Za-z]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
22
+ const RATE_TEXT = /^\d+(?:\.\d+)?$/;
23
+ const ISO_DAY = /^(\d{4})-(\d\d)-(\d\d)$/;
24
+ /**
25
+ * The ECB's rates are typed 'reference', not 'mid': they are set once a day, around 14:10 CET, from
26
+ * a regular concertation between central banks, and published for information only. They are not a
27
+ * market mid-rate that anyone deals at.
28
+ */
29
+ function ecbTable(asOf, rates) {
30
+ return rateTable({ base: 'EUR', asOf, source: 'ECB', type: 'reference', rates });
31
+ }
32
+ function notAnEcbFile() {
33
+ throw new SyntaxError('Not an ECB reference rate file');
34
+ }
35
+ function withoutByteOrderMark(text) {
36
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
37
+ }
38
+ /** The currency → rate attributes of the <Cube> elements in one day's cube. */
39
+ function ratesOfDay(body) {
40
+ const rates = {};
41
+ for (const [, attributeText] of body.matchAll(INNER_CUBE)) {
42
+ const attributes = {};
43
+ for (const [, name, doubleQuoted, singleQuoted] of attributeText.matchAll(ATTRIBUTE)) {
44
+ attributes[name] = doubleQuoted ?? singleQuoted;
45
+ }
46
+ const currency = attributes['currency'];
47
+ const rate = attributes['rate'];
48
+ if (currency === undefined && rate === undefined) {
49
+ continue;
50
+ }
51
+ // Fail rather than silently drop a currency: a missing rate would look like "not quoted".
52
+ if (currency === undefined ||
53
+ !/^[A-Z]{3}$/.test(currency) ||
54
+ rate === undefined ||
55
+ !RATE_TEXT.test(rate)) {
56
+ throw new SyntaxError(`Malformed ECB rate entry <Cube${attributeText}>`);
57
+ }
58
+ rates[currency] = rate;
59
+ }
60
+ return rates;
61
+ }
62
+ function newestFirst(tables) {
63
+ return tables.sort((table, other) => other.asOf.localeCompare(table.asOf));
64
+ }
65
+ /**
66
+ * Parses eurofxref-daily.xml, eurofxref-hist-90d.xml or eurofxref-hist.xml into one EUR-based table
67
+ * per day, newest first. Each rate is EUR → currency, with `source: 'ECB'`, `type: 'reference'` and
68
+ * `asOf` the publication day. A day without any rates is left out, so a lookback goes on to the day
69
+ * before. A rate entry that can't be read throws SyntaxError.
70
+ *
71
+ * const [latest] = parseEcbXml(await readFile('eurofxref-daily.xml', 'utf8'));
72
+ */
73
+ export function parseEcbXml(xml) {
74
+ let days = 0;
75
+ const tables = [];
76
+ for (const [, asOf, body] of withoutByteOrderMark(xml).matchAll(DAY_CUBE)) {
77
+ days++;
78
+ const day = isoDay(asOf);
79
+ const rates = ratesOfDay(body);
80
+ if (Object.keys(rates).length > 0) {
81
+ tables.push(ecbTable(day, rates));
82
+ }
83
+ }
84
+ if (days === 0) {
85
+ notAnEcbFile();
86
+ }
87
+ return newestFirst(tables);
88
+ }
89
+ /** The ISO date of year-month-day, or a SyntaxError when that day doesn't exist (32 November). */
90
+ function calendarDay(year, month, day) {
91
+ const date = new Date(Date.UTC(year, month - 1, day));
92
+ if (date.getUTCFullYear() !== year ||
93
+ date.getUTCMonth() !== month - 1 ||
94
+ date.getUTCDate() !== day) {
95
+ throw new SyntaxError(`Not a calendar day: ${year}-${month}-${day}`);
96
+ }
97
+ return date.toISOString().slice(0, 10);
98
+ }
99
+ /** '2026-11-06', or '06 November 2026' as in the daily CSV, as an ISO date. */
100
+ function isoDay(text) {
101
+ const iso = ISO_DAY.exec(text);
102
+ if (iso !== null) {
103
+ return calendarDay(Number(iso[1]), Number(iso[2]), Number(iso[3]));
104
+ }
105
+ const match = /^(\d{1,2}) ([A-Za-z]+) (\d{4})$/.exec(text);
106
+ if (match === null) {
107
+ notAnEcbFile();
108
+ }
109
+ const [, day, monthName, year] = match;
110
+ const month = MONTHS.indexOf(monthName.toLowerCase()) + 1;
111
+ if (month === 0) {
112
+ notAnEcbFile();
113
+ }
114
+ return calendarDay(Number(year), month, Number(day));
115
+ }
116
+ /**
117
+ * Parses eurofxref.csv or eurofxref-hist.csv (from the ECB's zips) the way parseEcbXml does.
118
+ * 'N/A' and blank cells (currencies not quoted that day) are skipped, and a day with no rates at
119
+ * all is left out, so a lookback goes on to the day before.
120
+ */
121
+ export function parseEcbCsv(csv) {
122
+ const lines = withoutByteOrderMark(csv)
123
+ .split(/\r?\n/)
124
+ .filter((line) => line.trim() !== '');
125
+ const columns = (lines[0] ?? '').split(',').map((cell) => cell.trim());
126
+ if (columns[0] !== 'Date') {
127
+ notAnEcbFile();
128
+ }
129
+ const tables = [];
130
+ for (const line of lines.slice(1)) {
131
+ const cells = line.split(',').map((cell) => cell.trim());
132
+ const rates = {};
133
+ for (let i = 1; i < columns.length; i++) {
134
+ const currency = columns[i];
135
+ const rate = cells[i];
136
+ if (currency && rate && rate !== 'N/A') {
137
+ rates[currency] = rate;
138
+ }
139
+ }
140
+ const day = isoDay(cells[0]);
141
+ if (Object.keys(rates).length > 0) {
142
+ tables.push(ecbTable(day, rates));
143
+ }
144
+ }
145
+ return newestFirst(tables);
146
+ }
147
+ //# sourceMappingURL=ecb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ecb.js","sourceRoot":"","sources":["../src/ecb.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,4FAA4F;AAC5F,OAAO,EAAE,SAAS,EAAkB,MAAM,eAAe,CAAC;AAE1D,MAAM,MAAM,GAAG;IACb,SAAS;IACT,UAAU;IACV,OAAO;IACP,OAAO;IACP,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,WAAW;IACX,SAAS;IACT,UAAU;IACV,UAAU;CACX,CAAC;AACF,MAAM,QAAQ,GAAG,+DAA+D,CAAC;AACjF,4FAA4F;AAC5F,MAAM,UAAU,GAAG,sBAAsB,CAAC;AAC1C,MAAM,SAAS,GAAG,4CAA4C,CAAC;AAC/D,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,MAAM,OAAO,GAAG,yBAAyB,CAAC;AAE1C;;;;GAIG;AACH,SAAS,QAAQ,CAAC,IAAY,EAAE,KAA6B;IAC3D,OAAO,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;AACnF,CAAC;AAED,SAAS,YAAY;IACnB,MAAM,IAAI,WAAW,CAAC,gCAAgC,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,+EAA+E;AAC/E,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,EAAE,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1D,MAAM,UAAU,GAA2B,EAAE,CAAC;QAC9C,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,CAAC,IAAI,aAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YACtF,UAAU,CAAC,IAAK,CAAC,GAAG,YAAY,IAAI,YAAa,CAAC;QACpD,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;QAChC,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACjD,SAAS;QACX,CAAC;QACD,0FAA0F;QAC1F,IACE,QAAQ,KAAK,SAAS;YACtB,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC5B,IAAI,KAAK,SAAS;YAClB,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EACrB,CAAC;YACD,MAAM,IAAI,WAAW,CAAC,iCAAiC,aAAa,GAAG,CAAC,CAAC;QAC3E,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IACzB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,MAAmB;IACtC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAK,CAAC,aAAa,CAAC,KAAK,CAAC,IAAK,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,oBAAoB,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1E,IAAI,EAAE,CAAC;QACP,MAAM,GAAG,GAAG,MAAM,CAAC,IAAK,CAAC,CAAC;QAC1B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAK,CAAC,CAAC;QAChC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QACf,YAAY,EAAE,CAAC;IACjB,CAAC;IACD,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED,kGAAkG;AAClG,SAAS,WAAW,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW;IAC3D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACtD,IACE,IAAI,CAAC,cAAc,EAAE,KAAK,IAAI;QAC9B,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,GAAG,CAAC;QAChC,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,EACzB,CAAC;QACD,MAAM,IAAI,WAAW,CAAC,uBAAuB,IAAI,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,+EAA+E;AAC/E,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,KAAK,GAAG,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,YAAY,EAAE,CAAC;IACjB,CAAC;IACD,MAAM,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,SAAU,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,YAAY,EAAE,CAAC;IACjB,CAAC;IACD,OAAO,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,KAAK,GAAG,oBAAoB,CAAC,GAAG,CAAC;SACpC,KAAK,CAAC,OAAO,CAAC;SACd,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACvE,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QAC1B,YAAY,EAAE,CAAC;IACjB,CAAC;IAED,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACzD,MAAM,KAAK,GAA2B,EAAE,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,IAAI,QAAQ,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;gBACvC,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;YACzB,CAAC;QACH,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;QAC9B,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { type Factor } from './decimal.js';
2
+ import { type Money } from './money.js';
3
+ import type { Rate } from './rate.js';
4
+ import { type RoundingMode } from './rounding.js';
5
+ /**
6
+ * A rounded value and what rounding left out: `value + residual` is exactly the unrounded amount.
7
+ */
8
+ export interface Rounded<C extends string> {
9
+ readonly value: Money<C>;
10
+ readonly residual: ExactMoney<C>;
11
+ }
12
+ /**
13
+ * An exact amount of `currency`: num/den major units, reduced, den > 0. Build it with `exact()`.
14
+ */
15
+ export declare class ExactMoney<C extends string = string> {
16
+ readonly currency: C;
17
+ readonly num: bigint;
18
+ readonly den: bigint;
19
+ /** @internal use exact() */
20
+ constructor(currency: C, numerator: bigint, denominator: bigint);
21
+ /** this + other, exactly. */
22
+ add(other: ExactMoney<C>): ExactMoney<C>;
23
+ /** this − other, exactly. */
24
+ subtract(other: ExactMoney<C>): ExactMoney<C>;
25
+ /** −this. */
26
+ negate(): ExactMoney<C>;
27
+ /** this × factor, exactly. */
28
+ multiply(factor: Factor): ExactMoney<C>;
29
+ /** this ÷ divisor, exactly. */
30
+ divide(divisor: Factor): ExactMoney<C>;
31
+ /** Converts exactly at `rate`; nothing is rounded. */
32
+ convert<To extends string>(rate: Rate<C, To>): ExactMoney<To>;
33
+ /** −1, 0 or 1. */
34
+ compare(other: ExactMoney<C>): -1 | 0 | 1;
35
+ /** Same currency and the same exact value. */
36
+ equals(other: ExactMoney): boolean;
37
+ /** −1, 0 or 1. */
38
+ sign(): -1 | 0 | 1;
39
+ /** Whether the amount is zero. */
40
+ isZero(): boolean;
41
+ /** Whether the amount fits `scale` (default: the currency's exponent) without rounding. */
42
+ isExact(scale?: number): boolean;
43
+ /** Rounds once to `scale` (default: the currency's exponent). */
44
+ round(rounding?: RoundingMode, scale?: number): Money<C>;
45
+ /** Rounds once and reports the difference, for booking it (e.g. an "FX rounding" ledger line). */
46
+ roundWithResidual(rounding?: RoundingMode, scale?: number): Rounded<C>;
47
+ /** "10/3 EUR", for debugging only. */
48
+ toString(): string;
49
+ /** Blocks `a + b` and `a > b`: round() first. */
50
+ [Symbol.toPrimitive](hint: string): string;
51
+ }
52
+ /**
53
+ * The exact value of `money`, for chained arithmetic: `exact(price).divide(3).round('halfEven')`.
54
+ */
55
+ export declare function exact<C extends string>(money: Money<C>): ExactMoney<C>;
56
+ /**
57
+ * The exact sum (zero of `currency` for an empty list). Round it once for a batch's rounding line.
58
+ */
59
+ export declare function sumExact<C extends string>(items: readonly ExactMoney<C>[], currency?: C): ExactMoney<C>;
60
+ /** Whether `value` is an ExactMoney. */
61
+ export declare function isExactMoney(value: unknown): value is ExactMoney;
62
+ //# sourceMappingURL=exact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact.d.ts","sourceRoot":"","sources":["../src/exact.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAA0C,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1F;;GAEG;AACH,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM;IACvC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAC/C,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,4BAA4B;gBAChB,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAO/D,6BAA6B;IAC7B,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;IAMxC,6BAA6B;IAC7B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;IAI7C,aAAa;IACb,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC;IAIvB,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC;IAKvC,+BAA+B;IAC/B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC;IAQtC,sDAAsD;IACtD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE,CAAC;IAO7D,kBAAkB;IAClB,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAczC,8CAA8C;IAC9C,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;IAIlC,kBAAkB;IAClB,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAUlB,kCAAkC;IAClC,MAAM,IAAI,OAAO;IAIjB,2FAA2F;IAC3F,OAAO,CAAC,KAAK,GAAE,MAAwC,GAAG,OAAO;IAIjE,iEAAiE;IACjE,KAAK,CACH,QAAQ,GAAE,YAA+B,EACzC,KAAK,GAAE,MAAwC,GAC9C,KAAK,CAAC,CAAC,CAAC;IAKX,kGAAkG;IAClG,iBAAiB,CAAC,QAAQ,GAAE,YAA+B,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAMxF,sCAAsC;IACtC,QAAQ,IAAI,MAAM;IAOlB,iDAAiD;IACjD,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAM3C;AAQD;;GAEG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAEtE;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EACvC,KAAK,EAAE,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,EAC/B,QAAQ,CAAC,EAAE,CAAC,GACX,UAAU,CAAC,CAAC,CAAC,CAUf;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE"}
package/dist/exact.js ADDED
@@ -0,0 +1,147 @@
1
+ // ExactMoney: an exact rational amount for chains of arithmetic that should round once, at the end.
2
+ // It has no formatting, JSON or minor units on purpose: it must be rounded back to Money before it
3
+ // leaves the process.
4
+ import { currencyExponent } from './currency.js';
5
+ import { pow10n, ratio } from './decimal.js';
6
+ import { CurrencyMismatchError, make } from './money.js';
7
+ import { DEFAULT_ROUNDING, divideAndRoundBigint } from './rounding.js';
8
+ /**
9
+ * An exact amount of `currency`: num/den major units, reduced, den > 0. Build it with `exact()`.
10
+ */
11
+ export class ExactMoney {
12
+ currency;
13
+ num;
14
+ den;
15
+ /** @internal use exact() */
16
+ constructor(currency, numerator, denominator) {
17
+ const reduced = ratio(numerator, denominator); // reduces and makes the denominator positive
18
+ this.currency = currency;
19
+ this.num = reduced.num;
20
+ this.den = reduced.den;
21
+ }
22
+ /** this + other, exactly. */
23
+ add(other) {
24
+ assertSameCurrency(this, other);
25
+ const numerator = this.num * other.den + other.num * this.den;
26
+ return new ExactMoney(this.currency, numerator, this.den * other.den);
27
+ }
28
+ /** this − other, exactly. */
29
+ subtract(other) {
30
+ return this.add(other.negate());
31
+ }
32
+ /** −this. */
33
+ negate() {
34
+ return new ExactMoney(this.currency, -this.num, this.den);
35
+ }
36
+ /** this × factor, exactly. */
37
+ multiply(factor) {
38
+ const factorRatio = ratio(factor);
39
+ return new ExactMoney(this.currency, this.num * factorRatio.num, this.den * factorRatio.den);
40
+ }
41
+ /** this ÷ divisor, exactly. */
42
+ divide(divisor) {
43
+ const divisorRatio = ratio(divisor);
44
+ if (divisorRatio.num === 0n) {
45
+ throw new RangeError('Division by zero');
46
+ }
47
+ return new ExactMoney(this.currency, this.num * divisorRatio.den, this.den * divisorRatio.num);
48
+ }
49
+ /** Converts exactly at `rate`; nothing is rounded. */
50
+ convert(rate) {
51
+ if (rate.from !== this.currency) {
52
+ throw new CurrencyMismatchError(rate.from, this.currency);
53
+ }
54
+ return new ExactMoney(rate.to, this.num * rate.ratio.num, this.den * rate.ratio.den);
55
+ }
56
+ /** −1, 0 or 1. */
57
+ compare(other) {
58
+ assertSameCurrency(this, other);
59
+ // Both denominators are positive, so cross-multiplying keeps the order.
60
+ const left = this.num * other.den;
61
+ const right = other.num * this.den;
62
+ if (left < right) {
63
+ return -1;
64
+ }
65
+ if (left > right) {
66
+ return 1;
67
+ }
68
+ return 0;
69
+ }
70
+ /** Same currency and the same exact value. */
71
+ equals(other) {
72
+ return this.currency === other.currency && this.num === other.num && this.den === other.den;
73
+ }
74
+ /** −1, 0 or 1. */
75
+ sign() {
76
+ if (this.num > 0n) {
77
+ return 1;
78
+ }
79
+ if (this.num < 0n) {
80
+ return -1;
81
+ }
82
+ return 0;
83
+ }
84
+ /** Whether the amount is zero. */
85
+ isZero() {
86
+ return this.num === 0n;
87
+ }
88
+ /** Whether the amount fits `scale` (default: the currency's exponent) without rounding. */
89
+ isExact(scale = currencyExponent(this.currency)) {
90
+ return (this.num * pow10n(scale)) % this.den === 0n;
91
+ }
92
+ /** Rounds once to `scale` (default: the currency's exponent). */
93
+ round(rounding = DEFAULT_ROUNDING, scale = currencyExponent(this.currency)) {
94
+ const minor = divideAndRoundBigint(this.num * pow10n(scale), this.den, rounding);
95
+ return make(this.currency, minor, scale);
96
+ }
97
+ /** Rounds once and reports the difference, for booking it (e.g. an "FX rounding" ledger line). */
98
+ roundWithResidual(rounding = DEFAULT_ROUNDING, scale) {
99
+ const value = this.round(rounding, scale);
100
+ const residual = this.subtract(exact(value));
101
+ return { value, residual };
102
+ }
103
+ /** "10/3 EUR", for debugging only. */
104
+ toString() {
105
+ if (this.den === 1n) {
106
+ return `${this.num} ${this.currency}`;
107
+ }
108
+ return `${this.num}/${this.den} ${this.currency}`;
109
+ }
110
+ /** Blocks `a + b` and `a > b`: round() first. */
111
+ [Symbol.toPrimitive](hint) {
112
+ if (hint === 'string') {
113
+ return this.toString();
114
+ }
115
+ throw new TypeError('ExactMoney cannot be converted to a number; round() it first');
116
+ }
117
+ }
118
+ function assertSameCurrency(money, other) {
119
+ if (money.currency !== other.currency) {
120
+ throw new CurrencyMismatchError(money.currency, other.currency);
121
+ }
122
+ }
123
+ /**
124
+ * The exact value of `money`, for chained arithmetic: `exact(price).divide(3).round('halfEven')`.
125
+ */
126
+ export function exact(money) {
127
+ return new ExactMoney(money.currency, BigInt(money.minor), pow10n(money.scale));
128
+ }
129
+ /**
130
+ * The exact sum (zero of `currency` for an empty list). Round it once for a batch's rounding line.
131
+ */
132
+ export function sumExact(items, currency) {
133
+ const sumCurrency = currency ?? items[0]?.currency;
134
+ if (sumCurrency === undefined) {
135
+ throw new RangeError('sumExact of an empty list needs a currency');
136
+ }
137
+ let sum = new ExactMoney(sumCurrency, 0n, 1n);
138
+ for (const item of items) {
139
+ sum = sum.add(item);
140
+ }
141
+ return sum;
142
+ }
143
+ /** Whether `value` is an ExactMoney. */
144
+ export function isExactMoney(value) {
145
+ return value instanceof ExactMoney;
146
+ }
147
+ //# sourceMappingURL=exact.js.map