@ducatjs/money 0.1.0 → 0.2.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 (201) hide show
  1. package/README.md +479 -62
  2. package/data/cash-rounding.json +123 -2
  3. package/data/cldr-cash.json +101 -0
  4. package/data/historic.json +78 -0
  5. package/data/iso4217-list-three.xml +1185 -0
  6. package/dist/adyen-fx.d.ts.map +1 -1
  7. package/dist/adyen-fx.js +22 -4
  8. package/dist/adyen-fx.js.map +1 -1
  9. package/dist/boc.d.ts +31 -0
  10. package/dist/boc.d.ts.map +1 -0
  11. package/dist/boc.js +114 -0
  12. package/dist/boc.js.map +1 -0
  13. package/dist/boe.d.ts +33 -0
  14. package/dist/boe.d.ts.map +1 -0
  15. package/dist/boe.js +114 -0
  16. package/dist/boe.js.map +1 -0
  17. package/dist/cash.d.ts +30 -1
  18. package/dist/cash.d.ts.map +1 -1
  19. package/dist/cash.js +50 -2
  20. package/dist/cash.js.map +1 -1
  21. package/dist/convert.d.ts +10 -9
  22. package/dist/convert.d.ts.map +1 -1
  23. package/dist/convert.js +13 -3
  24. package/dist/convert.js.map +1 -1
  25. package/dist/drizzle/mysql.d.ts +105 -0
  26. package/dist/drizzle/mysql.d.ts.map +1 -0
  27. package/dist/drizzle/mysql.js +60 -0
  28. package/dist/drizzle/mysql.js.map +1 -0
  29. package/dist/drizzle/pg.d.ts +103 -0
  30. package/dist/drizzle/pg.d.ts.map +1 -0
  31. package/dist/drizzle/pg.js +60 -0
  32. package/dist/drizzle/pg.js.map +1 -0
  33. package/dist/drizzle/sqlite.d.ts +102 -0
  34. package/dist/drizzle/sqlite.d.ts.map +1 -0
  35. package/dist/drizzle/sqlite.js +59 -0
  36. package/dist/drizzle/sqlite.js.map +1 -0
  37. package/dist/ecb.d.ts +37 -2
  38. package/dist/ecb.d.ts.map +1 -1
  39. package/dist/ecb.js +148 -10
  40. package/dist/ecb.js.map +1 -1
  41. package/dist/fed.d.ts +35 -0
  42. package/dist/fed.d.ts.map +1 -0
  43. package/dist/fed.js +134 -0
  44. package/dist/fed.js.map +1 -0
  45. package/dist/fetch.d.ts +45 -0
  46. package/dist/fetch.d.ts.map +1 -1
  47. package/dist/fetch.js +167 -24
  48. package/dist/fetch.js.map +1 -1
  49. package/dist/fn.d.ts +8 -1
  50. package/dist/fn.d.ts.map +1 -1
  51. package/dist/fn.js +8 -1
  52. package/dist/fn.js.map +1 -1
  53. package/dist/generated/cash.d.ts +2 -0
  54. package/dist/generated/cash.d.ts.map +1 -1
  55. package/dist/generated/cash.js +101 -0
  56. package/dist/generated/cash.js.map +1 -1
  57. package/dist/generated/historic.d.ts +337 -0
  58. package/dist/generated/historic.d.ts.map +1 -0
  59. package/dist/generated/historic.js +66 -0
  60. package/dist/generated/historic.js.map +1 -0
  61. package/dist/historic.d.ts +40 -0
  62. package/dist/historic.d.ts.map +1 -0
  63. package/dist/historic.js +43 -0
  64. package/dist/historic.js.map +1 -0
  65. package/dist/history.d.ts +96 -0
  66. package/dist/history.d.ts.map +1 -0
  67. package/dist/history.js +213 -0
  68. package/dist/history.js.map +1 -0
  69. package/dist/hmrc.d.ts +28 -0
  70. package/dist/hmrc.d.ts.map +1 -0
  71. package/dist/hmrc.js +108 -0
  72. package/dist/hmrc.js.map +1 -0
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/kysely.d.ts +120 -0
  78. package/dist/kysely.d.ts.map +1 -0
  79. package/dist/kysely.js +309 -0
  80. package/dist/kysely.js.map +1 -0
  81. package/dist/money.d.ts +93 -9
  82. package/dist/money.d.ts.map +1 -1
  83. package/dist/money.js +135 -9
  84. package/dist/money.js.map +1 -1
  85. package/dist/parse.d.ts +45 -1
  86. package/dist/parse.d.ts.map +1 -1
  87. package/dist/parse.js +290 -3
  88. package/dist/parse.js.map +1 -1
  89. package/dist/pg.d.ts +100 -0
  90. package/dist/pg.d.ts.map +1 -0
  91. package/dist/pg.js +343 -0
  92. package/dist/pg.js.map +1 -0
  93. package/dist/plugins/all.d.ts +8 -1
  94. package/dist/plugins/all.d.ts.map +1 -1
  95. package/dist/plugins/all.js +8 -1
  96. package/dist/plugins/all.js.map +1 -1
  97. package/dist/plugins/convert.d.ts +3 -4
  98. package/dist/plugins/convert.d.ts.map +1 -1
  99. package/dist/plugins/convert.js.map +1 -1
  100. package/dist/plugins/tax.d.ts +4 -5
  101. package/dist/plugins/tax.d.ts.map +1 -1
  102. package/dist/plugins/tax.js.map +1 -1
  103. package/dist/policy.d.ts +34 -0
  104. package/dist/policy.d.ts.map +1 -0
  105. package/dist/policy.js +90 -0
  106. package/dist/policy.js.map +1 -0
  107. package/dist/prisma.d.ts +25 -14
  108. package/dist/prisma.d.ts.map +1 -1
  109. package/dist/prisma.js +29 -19
  110. package/dist/prisma.js.map +1 -1
  111. package/dist/provider.d.ts +21 -7
  112. package/dist/provider.d.ts.map +1 -1
  113. package/dist/provider.js +51 -11
  114. package/dist/provider.js.map +1 -1
  115. package/dist/rate-text.d.ts +38 -0
  116. package/dist/rate-text.d.ts.map +1 -0
  117. package/dist/rate-text.js +143 -0
  118. package/dist/rate-text.js.map +1 -0
  119. package/dist/rate.d.ts +61 -5
  120. package/dist/rate.d.ts.map +1 -1
  121. package/dist/rate.js +133 -33
  122. package/dist/rate.js.map +1 -1
  123. package/dist/rounding.js +1 -1
  124. package/dist/rounding.js.map +1 -1
  125. package/dist/schema.d.ts +43 -0
  126. package/dist/schema.d.ts.map +1 -0
  127. package/dist/schema.js +77 -0
  128. package/dist/schema.js.map +1 -0
  129. package/dist/sql.d.ts +54 -0
  130. package/dist/sql.d.ts.map +1 -0
  131. package/dist/sql.js +131 -0
  132. package/dist/sql.js.map +1 -0
  133. package/dist/tax.d.ts +13 -9
  134. package/dist/tax.d.ts.map +1 -1
  135. package/dist/tax.js +20 -4
  136. package/dist/tax.js.map +1 -1
  137. package/dist/testing/arbitraries.d.ts +43 -0
  138. package/dist/testing/arbitraries.d.ts.map +1 -0
  139. package/dist/testing/arbitraries.js +98 -0
  140. package/dist/testing/arbitraries.js.map +1 -0
  141. package/dist/testing/index.d.ts +67 -0
  142. package/dist/testing/index.d.ts.map +1 -0
  143. package/dist/testing/index.js +159 -0
  144. package/dist/testing/index.js.map +1 -0
  145. package/dist/testing/jest.d.ts +2 -0
  146. package/dist/testing/jest.d.ts.map +1 -0
  147. package/dist/testing/jest.js +15 -0
  148. package/dist/testing/jest.js.map +1 -0
  149. package/dist/testing/vitest.d.ts +7 -0
  150. package/dist/testing/vitest.d.ts.map +1 -0
  151. package/dist/testing/vitest.js +12 -0
  152. package/dist/testing/vitest.js.map +1 -0
  153. package/dist/valibot.d.ts +33 -0
  154. package/dist/valibot.d.ts.map +1 -0
  155. package/dist/valibot.js +61 -0
  156. package/dist/valibot.js.map +1 -0
  157. package/dist/zod.d.ts +36 -0
  158. package/dist/zod.d.ts.map +1 -0
  159. package/dist/zod.js +58 -0
  160. package/dist/zod.js.map +1 -0
  161. package/package.json +116 -5
  162. package/src/adyen-fx.ts +25 -4
  163. package/src/boc.ts +136 -0
  164. package/src/boe.ts +131 -0
  165. package/src/cash.ts +70 -2
  166. package/src/convert.ts +33 -15
  167. package/src/drizzle/mysql.ts +78 -0
  168. package/src/drizzle/pg.ts +78 -0
  169. package/src/drizzle/sqlite.ts +70 -0
  170. package/src/ecb.ts +179 -13
  171. package/src/fed.ts +146 -0
  172. package/src/fetch.ts +207 -28
  173. package/src/fn.ts +14 -0
  174. package/src/generated/cash.ts +103 -0
  175. package/src/generated/historic.ts +67 -0
  176. package/src/historic.ts +77 -0
  177. package/src/history.ts +333 -0
  178. package/src/hmrc.ts +125 -0
  179. package/src/index.ts +4 -0
  180. package/src/kysely.ts +588 -0
  181. package/src/money.ts +210 -12
  182. package/src/parse.ts +371 -4
  183. package/src/pg.ts +410 -0
  184. package/src/plugins/all.ts +10 -0
  185. package/src/plugins/convert.ts +4 -5
  186. package/src/plugins/tax.ts +9 -10
  187. package/src/policy.ts +124 -0
  188. package/src/prisma.ts +91 -10
  189. package/src/provider.ts +81 -16
  190. package/src/rate-text.ts +151 -0
  191. package/src/rate.ts +205 -36
  192. package/src/rounding.ts +1 -1
  193. package/src/schema.ts +119 -0
  194. package/src/sql.ts +178 -0
  195. package/src/tax.ts +40 -9
  196. package/src/testing/arbitraries.ts +134 -0
  197. package/src/testing/index.ts +247 -0
  198. package/src/testing/jest.ts +18 -0
  199. package/src/testing/vitest.ts +18 -0
  200. package/src/valibot.ts +87 -0
  201. package/src/zod.ts +78 -0
package/src/fetch.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  // Network fetchers for published reference rates, using the platform fetch. Opt-in: nothing else in
2
2
  // the library imports this module, so core and @ducatjs/money/convert never touch the
3
3
  // network. You decide when to call it and how to cache.
4
- import { parseEcbXml } from './ecb.js';
4
+ import { parseBankOfCanadaJson } from './boc.js';
5
+ import { parseEcbSdmxCsv, parseEcbSdmxDecimals, parseEcbXml } from './ecb.js';
5
6
  import type { RateTable } from './provider.js';
6
7
 
7
8
  /**
@@ -73,33 +74,8 @@ export class FetchRatesError extends Error {
73
74
  */
74
75
  export async function fetchEcb(options?: FetchEcbOptions): Promise<RateTable[]> {
75
76
  const url = ECB_URLS[options?.history ?? 'daily'];
76
- const fetchFunction = options?.fetch ?? (globalThis as { fetch?: Fetch }).fetch;
77
- if (fetchFunction === undefined) {
78
- throw new TypeError('No fetch available: pass options.fetch');
79
- }
80
-
81
- const init: { signal?: AbortSignalLike; headers: Record<string, string> } = {
82
- headers: { accept: 'application/xml' },
83
- };
84
- const signal = requestSignal(options?.signal, options?.timeout ?? DEFAULT_TIMEOUT_MS);
85
- if (signal !== undefined) {
86
- init.signal = signal;
87
- }
88
- const response = await fetchFunction(url, init);
89
- if (!response.ok) {
90
- throw new FetchRatesError(url, response.status);
91
- }
92
-
93
- const body = await response.text();
94
- try {
95
- return parseEcbXml(body);
96
- } catch (error) {
97
- // An error page or a captive portal is the usual cause, so say where the bad file came from.
98
- if (error instanceof SyntaxError) {
99
- throw new SyntaxError(`${error.message} (from ${url})`, { cause: error });
100
- }
101
- throw error;
102
- }
77
+ const body = await getText(url, 'application/xml', options);
78
+ return parsedFrom(url, () => parseEcbXml(body));
103
79
  }
104
80
 
105
81
  /** The caller's signal, a timeout signal, or both combined. */
@@ -125,3 +101,206 @@ function requestSignal(
125
101
  }
126
102
  return statics.any([signal, timeoutSignal]);
127
103
  }
104
+
105
+ /** The ECB Data Portal's SDMX REST API (no key, and CORS-enabled, so it works in browsers). */
106
+ export const ECB_DATA_API = 'https://data-api.ecb.europa.eu/service/data/EXR';
107
+
108
+ /** Options for fetching a date range of ECB reference rates. */
109
+ export interface FetchEcbRangeOptions extends Omit<FetchEcbOptions, 'history'> {
110
+ /** The first day ('1999-01-04' at the earliest). */
111
+ readonly start: string | Date;
112
+ /** The last day (default: the latest published). */
113
+ readonly end?: string | Date;
114
+ /** ISO codes to fetch (default: every currency, including discontinued series). */
115
+ readonly currencies?: readonly string[];
116
+ }
117
+
118
+ /**
119
+ * A request's UTC day: a 'YYYY-MM-DD' string that is a real calendar day, or a Date (any
120
+ * offset) as its UTC day. Anything else is a RangeError, before any request is made.
121
+ */
122
+ function requestDay(time: string | Date, name: string): string {
123
+ let day: string;
124
+ if (typeof time === 'string') {
125
+ day = time;
126
+ } else {
127
+ day = Number.isNaN(time.getTime()) ? '' : time.toISOString().slice(0, 10);
128
+ }
129
+ const match = /^(\d{4})-(\d\d)-(\d\d)$/.exec(day);
130
+ const date =
131
+ match === null ? undefined : new Date(Date.UTC(+match[1]!, +match[2]! - 1, +match[3]!));
132
+ // Date.UTC rolls 30 February over into March, so a real day must come back unchanged.
133
+ if (date === undefined || date.toISOString().slice(0, 10) !== day) {
134
+ throw new RangeError(`${name} must be a date, got ${String(time)}`);
135
+ }
136
+ return day;
137
+ }
138
+
139
+ /** Wraps a parser's SyntaxError with the URL the bad file came from (an error page, usually). */
140
+ function parsedFrom<T>(url: string, parse: () => T): T {
141
+ try {
142
+ return parse();
143
+ } catch (error) {
144
+ if (error instanceof SyntaxError) {
145
+ throw new SyntaxError(`${error.message} (from ${url})`, { cause: error });
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /** The platform's AbortController, where it exists. */
152
+ type AbortControllerLike = { readonly signal: AbortSignalLike; abort(reason?: unknown): void };
153
+
154
+ /** GETs `url` as text, with the timeout and signal handling every fetcher here shares. */
155
+ async function getText(
156
+ url: string,
157
+ accept: string,
158
+ options: Omit<FetchEcbOptions, 'history'> | undefined,
159
+ ): Promise<string> {
160
+ return (await getTextOrNotFound(url, accept, options, false))!;
161
+ }
162
+
163
+ /** getText, or undefined for HTTP 404 when `notFoundIsEmpty` (an API's "no results"). */
164
+ async function getTextOrNotFound(
165
+ url: string,
166
+ accept: string,
167
+ options: Omit<FetchEcbOptions, 'history'> | undefined,
168
+ notFoundIsEmpty: boolean,
169
+ ): Promise<string | undefined> {
170
+ const fetchFunction = options?.fetch ?? (globalThis as { fetch?: Fetch }).fetch;
171
+ if (fetchFunction === undefined) {
172
+ throw new TypeError('No fetch available: pass options.fetch');
173
+ }
174
+ const init: { signal?: AbortSignalLike; headers: Record<string, string> } = {
175
+ headers: { accept },
176
+ };
177
+ const signal = requestSignal(options?.signal, options?.timeout ?? DEFAULT_TIMEOUT_MS);
178
+ if (signal !== undefined) {
179
+ init.signal = signal;
180
+ }
181
+ const response = await fetchFunction(url, init);
182
+ if (notFoundIsEmpty && response.status === 404) {
183
+ return undefined;
184
+ }
185
+ if (!response.ok) {
186
+ throw new FetchRatesError(url, response.status);
187
+ }
188
+ return response.text();
189
+ }
190
+
191
+ /**
192
+ * Fetches a date range of ECB euro reference rates from the ECB Data Portal and parses them with
193
+ * parseEcbSdmxCsv: EUR-based tables, one per TARGET working day, newest first, with the published
194
+ * digits. It makes two requests: the series' DECIMALS (small) and the observations without
195
+ * attributes, which keeps the full history since 1999 to about 15 MB. Give a few days before the
196
+ * first day you need, so that a lookback from it has a table to fall back on. A range with no
197
+ * rates (a weekend, or after the latest day) gives []. Both requests share one timeout and are
198
+ * cancelled together: if one fails, the other is aborted. The ECB states that its rates are for
199
+ * information purposes only.
200
+ *
201
+ * const tables = await fetchEcbRange({ start: '2026-01-01', currencies: ['USD', 'JPY'] });
202
+ * const history = rateHistory(tables); // from @ducatjs/money/convert/history
203
+ */
204
+ export async function fetchEcbRange(options: FetchEcbRangeOptions): Promise<RateTable[]> {
205
+ const currencies = options.currencies ?? [];
206
+ for (const currency of currencies) {
207
+ if (!/^[A-Z]{3}$/.test(currency)) {
208
+ throw new RangeError(`Not an ISO currency code: ${currency}`);
209
+ }
210
+ }
211
+ const start = requestDay(options.start, 'start');
212
+ if (start < '1999-01-04') {
213
+ throw new RangeError(`The ECB's rates start on 1999-01-04, not ${start}`);
214
+ }
215
+ let query = `?format=csvdata&startPeriod=${start}`;
216
+ if (options.end !== undefined) {
217
+ const end = requestDay(options.end, 'end');
218
+ if (end < start) {
219
+ throw new RangeError(`end ${end} is before start ${start}`);
220
+ }
221
+ query += `&endPeriod=${end}`;
222
+ }
223
+ const url = `${ECB_DATA_API}/D.${currencies.join('+')}.EUR.SP00.A`;
224
+ const seriesUrl = `${url}?format=csvdata&detail=nodata`;
225
+ const dataUrl = `${url}${query}&detail=dataonly`;
226
+
227
+ // One controller for both requests, tied to the caller's signal and the timeout, so that a
228
+ // failure of either cancels the other (the data can be ~15 MB) instead of letting it run on.
229
+ let shared: Omit<FetchEcbOptions, 'history'> = options;
230
+ let controller: AbortControllerLike | undefined;
231
+ const Controller = (globalThis as { AbortController?: new () => AbortControllerLike })
232
+ .AbortController;
233
+ if (Controller !== undefined) {
234
+ const own = new Controller();
235
+ controller = own;
236
+ const outer = requestSignal(options.signal, options.timeout ?? DEFAULT_TIMEOUT_MS);
237
+ if (outer?.aborted === true) {
238
+ own.abort(outer.reason);
239
+ } else {
240
+ outer?.addEventListener('abort', () => own.abort(outer.reason), { once: true });
241
+ }
242
+ shared = { ...options, signal: own.signal, timeout: 0 };
243
+ }
244
+
245
+ let series: string;
246
+ let data: string | undefined;
247
+ try {
248
+ [series, data] = await Promise.all([
249
+ getText(seriesUrl, 'text/csv', shared),
250
+ getTextOrNotFound(dataUrl, 'text/csv', shared, true),
251
+ ]);
252
+ } catch (error) {
253
+ controller?.abort(error);
254
+ throw error;
255
+ }
256
+ if (data === undefined) {
257
+ return [];
258
+ }
259
+ const decimals = parsedFrom(seriesUrl, () => parseEcbSdmxDecimals(series));
260
+ return parsedFrom(dataUrl, () => parseEcbSdmxCsv(data, { decimals }));
261
+ }
262
+
263
+ /** The Bank of Canada's Valet API for its daily exchange rates (no key; CORS-enabled). */
264
+ export const BANK_OF_CANADA_URL =
265
+ 'https://www.bankofcanada.ca/valet/observations/group/FX_RATES_DAILY/json';
266
+
267
+ /** Options for fetching Bank of Canada rates. */
268
+ export interface FetchBankOfCanadaOptions extends Omit<FetchEcbOptions, 'history'> {
269
+ /** The first day (2017-01-03 at the earliest). Default: only the latest published day. */
270
+ readonly start?: string | Date;
271
+ /** The last day (default: the latest published). Needs a `start`. */
272
+ readonly end?: string | Date;
273
+ }
274
+
275
+ /**
276
+ * Fetches the Bank of Canada's daily exchange rates from its Valet API and parses them with
277
+ * parseBankOfCanadaJson: CAD-based tables, newest first. Without `start`, only the latest day;
278
+ * without `end`, up to the latest. A range with no business days in it gives [].
279
+ * The Bank asks users to keep within its request limits, which it doesn't publish, and its rates
280
+ * are "released for statistical and analytical purposes only", not for transactions.
281
+ *
282
+ * const [latest] = await fetchBankOfCanada();
283
+ * const year = await fetchBankOfCanada({ start: '2026-01-01' });
284
+ */
285
+ export async function fetchBankOfCanada(options?: FetchBankOfCanadaOptions): Promise<RateTable[]> {
286
+ let query = '?recent=1';
287
+ if (options?.start !== undefined) {
288
+ const start = requestDay(options.start, 'start');
289
+ if (start < '2017-01-03') {
290
+ throw new RangeError(`Valet's daily rates start on 2017-01-03, not ${start}`);
291
+ }
292
+ query = `?start_date=${start}`;
293
+ if (options.end !== undefined) {
294
+ const end = requestDay(options.end, 'end');
295
+ if (end < start) {
296
+ throw new RangeError(`end ${end} is before start ${start}`);
297
+ }
298
+ query += `&end_date=${end}`;
299
+ }
300
+ } else if (options?.end !== undefined) {
301
+ throw new RangeError('end needs a start');
302
+ }
303
+ const url = `${BANK_OF_CANADA_URL}${query}`;
304
+ const body = await getText(url, 'application/json', options);
305
+ return parsedFrom(url, () => parseBankOfCanadaJson(body));
306
+ }
package/src/fn.ts CHANGED
@@ -12,6 +12,8 @@ export {
12
12
  zero,
13
13
  fromJSON,
14
14
  isMoney,
15
+ isMoneyIn,
16
+ assertCurrency,
15
17
  extendMoney,
16
18
  type ExtendMoneyOptions,
17
19
  sum,
@@ -26,6 +28,7 @@ export {
26
28
  toScale,
27
29
  compare,
28
30
  equals,
31
+ identical,
29
32
  lessThan,
30
33
  lessThanOrEqual,
31
34
  greaterThan,
@@ -37,10 +40,14 @@ export {
37
40
  min,
38
41
  max,
39
42
  clamp,
43
+ majorPart,
44
+ minorPart,
40
45
  toDecimal,
41
46
  toMinor,
42
47
  toMinorNumber,
43
48
  CurrencyMismatchError,
49
+ type RoundingPolicy,
50
+ type RoundingInput,
44
51
  type Money,
45
52
  type MoneyJSON,
46
53
  type MoneyOptions,
@@ -65,9 +72,16 @@ export * from './tax.js';
65
72
  export * from './convert.js';
66
73
  export * from './exact.js';
67
74
  export * from './ecb.js';
75
+ export * from './boc.js';
76
+ export * from './boe.js';
77
+ export * from './hmrc.js';
78
+ export * from './fed.js';
79
+ export * from './history.js';
68
80
  export * from './stripe-fx.js';
69
81
  export * from './adyen-fx.js';
70
82
  export * from './cash.js';
83
+ export * from './historic.js';
84
+ export { roundingPolicy, isoPolicy, assertScale, type RoundingPolicyOptions } from './policy.js';
71
85
  export * from './format.js';
72
86
  export * from './parse.js';
73
87
  export * from './stripe.js';
@@ -10,6 +10,8 @@ export interface CashRule {
10
10
  /** false: only secondary sources were found. */
11
11
  readonly verified: boolean;
12
12
  readonly source: string;
13
+ /** The tie rule the source prescribes, where it isn't halfExpand. */
14
+ readonly rounding?: import('../rounding.js').RoundingMode;
13
15
  }
14
16
 
15
17
  export const CASH_RULES: readonly CashRule[] = [
@@ -371,5 +373,106 @@ export const CASH_RULES: readonly CashRule[] = [
371
373
  "scope": "cash (permitted, not required; varies by state)",
372
374
  "verified": false,
373
375
  "source": "https://en.wikipedia.org/wiki/Cash_rounding"
376
+ },
377
+ {
378
+ "currency": "AMD",
379
+ "increment": 1000,
380
+ "countries": [
381
+ "AM"
382
+ ],
383
+ "scope": "cash (practice)",
384
+ "verified": true,
385
+ "source": "https://old.cba.am/en/SitePages/nccracoinscirculated.aspx"
386
+ },
387
+ {
388
+ "currency": "CRC",
389
+ "increment": 1000,
390
+ "countries": [
391
+ "CR"
392
+ ],
393
+ "scope": "cash (practice)",
394
+ "verified": true,
395
+ "source": "https://www.bccr.fi.cr/cr/es/noticias/listado-de-noticias/2026/monedas-de-5-10-y-25-del-diseno-anterior-dejaran-de-funcionar-como-medio-de-pago-el-1-de-julio-del-2.html"
396
+ },
397
+ {
398
+ "currency": "GYD",
399
+ "increment": 100,
400
+ "countries": [
401
+ "GY"
402
+ ],
403
+ "scope": "cash",
404
+ "verified": false,
405
+ "source": "https://bankofguyana.org.gy/bog/images/communications/press_release/Notice%20to%20stop%20minting%20one%20dollar%20coin.pdf"
406
+ },
407
+ {
408
+ "currency": "MNT",
409
+ "increment": 100,
410
+ "countries": [
411
+ "MN"
412
+ ],
413
+ "scope": "cash",
414
+ "verified": true,
415
+ "source": "https://www.mongolbank.mn/en/p/1416"
416
+ },
417
+ {
418
+ "currency": "MRU",
419
+ "increment": 20,
420
+ "countries": [
421
+ "MR"
422
+ ],
423
+ "scope": "cash",
424
+ "verified": false,
425
+ "source": "https://cridem.org/media/documents/bcm-presentation-gr-2017.pdf"
426
+ },
427
+ {
428
+ "currency": "PKR",
429
+ "increment": 100,
430
+ "countries": [
431
+ "PK"
432
+ ],
433
+ "scope": "cash",
434
+ "verified": true,
435
+ "source": "https://www.sbp.org.pk/press/2014/Coins-15-Sep-2014.pdf"
436
+ },
437
+ {
438
+ "currency": "RSD",
439
+ "increment": 100,
440
+ "countries": [
441
+ "RS"
442
+ ],
443
+ "scope": "cash (statutory)",
444
+ "verified": true,
445
+ "source": "https://www.nbs.rs/export/sites/NBS_site/documents/propisi/propisi-trz/opticaj_povlacenje_50_para.pdf",
446
+ "rounding": "halfTrunc"
447
+ },
448
+ {
449
+ "currency": "TZS",
450
+ "increment": 5000,
451
+ "countries": [
452
+ "TZ"
453
+ ],
454
+ "scope": "cash (practice)",
455
+ "verified": false,
456
+ "source": "https://www.bot.go.tz/Publications/Regular/Quarterly%20statistical%20Bulletin/en/2025090409030924.pdf"
457
+ },
458
+ {
459
+ "currency": "TWD",
460
+ "increment": 100,
461
+ "countries": [
462
+ "TW"
463
+ ],
464
+ "scope": "cash (practice)",
465
+ "verified": true,
466
+ "source": "https://www.cbc.gov.tw/tw/cp-769-26060-37841-1.html"
467
+ },
468
+ {
469
+ "currency": "UZS",
470
+ "increment": 5000,
471
+ "countries": [
472
+ "UZ"
473
+ ],
474
+ "scope": "cash (practice)",
475
+ "verified": true,
476
+ "source": "https://cbu.uz/en/banknotes-coins/coins/"
374
477
  }
375
478
  ];
@@ -0,0 +1,67 @@
1
+ // GENERATED by scripts/gen-currencies.mjs from data/iso4217-list-three.xml and data/historic.json. Do not edit by hand.
2
+ /* eslint-disable */
3
+
4
+ /** Publication date of the ISO 4217 List Three snapshot. */
5
+ export const ISO_4217_LIST_THREE_PUBLISHED = "2026-01-01";
6
+
7
+ /**
8
+ * Withdrawn ISO 4217 codes (with `numeric` and `withdrawn`, the year and month ISO lists) and
9
+ * non-ISO codes (without them), each with the exponent ISO gave it while it was current.
10
+ */
11
+ export const HISTORIC_CURRENCY_TABLE = {
12
+ ADP: {"name":"Andorran Peseta","numeric":"020","exponent":0,"withdrawn":"2003-07"},
13
+ AFA: {"name":"Afghani","numeric":"004","exponent":2,"withdrawn":"2003-01"},
14
+ ANG: {"name":"Netherlands Antillean Guilder","numeric":"532","exponent":2,"withdrawn":"2025-03"},
15
+ ATS: {"name":"Schilling","numeric":"040","exponent":2,"withdrawn":"2002-03"},
16
+ AYM: {"name":"Azerbaijan Manat","numeric":"945","exponent":2,"withdrawn":"2005-10"},
17
+ AZM: {"name":"Azerbaijanian Manat","numeric":"031","exponent":2,"withdrawn":"2005-12"},
18
+ BEF: {"name":"Belgian Franc","numeric":"056","exponent":0,"withdrawn":"2002-03"},
19
+ BGL: {"name":"Lev","numeric":"100","exponent":2,"withdrawn":"2003-11"},
20
+ BGN: {"name":"Bulgarian Lev","numeric":"975","exponent":2,"withdrawn":"2026-01"},
21
+ BYB: {"name":"Belarusian Ruble","numeric":"112","exponent":0,"withdrawn":"2001-01"},
22
+ BYR: {"name":"Belarusian Ruble","numeric":"974","exponent":0,"withdrawn":"2017-01"},
23
+ CNH: {"name":"Chinese Yuan Renminbi (offshore)","exponent":2},
24
+ CSD: {"name":"Serbian Dinar","numeric":"891","exponent":2,"withdrawn":"2006-10"},
25
+ CUC: {"name":"Peso Convertible","numeric":"931","exponent":2,"withdrawn":"2021-06"},
26
+ CYP: {"name":"Cyprus Pound","numeric":"196","exponent":2,"withdrawn":"2008-01"},
27
+ DEM: {"name":"Deutsche Mark","numeric":"276","exponent":2,"withdrawn":"2002-03"},
28
+ EEK: {"name":"Kroon","numeric":"233","exponent":2,"withdrawn":"2011-01"},
29
+ ESP: {"name":"Spanish Peseta","numeric":"724","exponent":0,"withdrawn":"2002-03"},
30
+ FIM: {"name":"Markka","numeric":"246","exponent":2,"withdrawn":"2002-03"},
31
+ FRF: {"name":"French Franc","numeric":"250","exponent":2,"withdrawn":"2002-03"},
32
+ GHC: {"name":"Cedi","numeric":"288","exponent":2,"withdrawn":"2008-01"},
33
+ GRD: {"name":"Drachma","numeric":"300","exponent":0,"withdrawn":"2002-03"},
34
+ GWP: {"name":"Guinea-Bissau Peso","numeric":"624","exponent":2,"withdrawn":"1997-05"},
35
+ HRK: {"name":"Kuna","numeric":"191","exponent":2,"withdrawn":"2023-01"},
36
+ IEP: {"name":"Irish Pound","numeric":"372","exponent":2,"withdrawn":"2002-03"},
37
+ ITL: {"name":"Italian Lira","numeric":"380","exponent":0,"withdrawn":"2002-03"},
38
+ LTL: {"name":"Lithuanian Litas","numeric":"440","exponent":2,"withdrawn":"2014-12"},
39
+ LUF: {"name":"Luxembourg Franc","numeric":"442","exponent":0,"withdrawn":"2002-03"},
40
+ LVL: {"name":"Latvian Lats","numeric":"428","exponent":2,"withdrawn":"2014-01"},
41
+ MGF: {"name":"Malagasy Franc","numeric":"450","exponent":0,"withdrawn":"2004-12"},
42
+ MRO: {"name":"Ouguiya","numeric":"478","exponent":2,"withdrawn":"2017-12"},
43
+ MTL: {"name":"Maltese Lira","numeric":"470","exponent":2,"withdrawn":"2008-01"},
44
+ MZM: {"name":"Mozambique Metical","numeric":"508","exponent":2,"withdrawn":"2006-06"},
45
+ NLG: {"name":"Netherlands Guilder","numeric":"528","exponent":2,"withdrawn":"2002-03"},
46
+ PTE: {"name":"Portuguese Escudo","numeric":"620","exponent":0,"withdrawn":"2002-03"},
47
+ ROL: {"name":"Old Leu","numeric":"642","exponent":0,"withdrawn":"2005-06"},
48
+ RUR: {"name":"Russian Ruble","numeric":"810","exponent":2,"withdrawn":"2004-01"},
49
+ SDD: {"name":"Sudanese Dinar","numeric":"736","exponent":2,"withdrawn":"2007-07"},
50
+ SIT: {"name":"Tolar","numeric":"705","exponent":2,"withdrawn":"2007-01"},
51
+ SKK: {"name":"Slovak Koruna","numeric":"703","exponent":2,"withdrawn":"2009-01"},
52
+ SLL: {"name":"Leone","numeric":"694","exponent":2,"withdrawn":"2023-12"},
53
+ SRG: {"name":"Surinam Guilder","numeric":"740","exponent":2,"withdrawn":"2003-12"},
54
+ STD: {"name":"Dobra","numeric":"678","exponent":2,"withdrawn":"2017-12"},
55
+ TMM: {"name":"Turkmenistan Manat","numeric":"795","exponent":2,"withdrawn":"2009-01"},
56
+ TPE: {"name":"Timor Escudo","numeric":"626","exponent":0,"withdrawn":"2002-11"},
57
+ TRL: {"name":"Old Turkish Lira","numeric":"792","exponent":0,"withdrawn":"2005-12"},
58
+ USS: {"name":"US Dollar (Same day)","numeric":"998","exponent":2,"withdrawn":"2014-03"},
59
+ VEB: {"name":"Bolivar","numeric":"862","exponent":2,"withdrawn":"2008-01"},
60
+ VEF: {"name":"Bolívar","numeric":"937","exponent":2,"withdrawn":"2018-08"},
61
+ YUM: {"name":"New Dinar","numeric":"891","exponent":2,"withdrawn":"2003-07"},
62
+ ZMK: {"name":"Zambian Kwacha","numeric":"894","exponent":2,"withdrawn":"2012-12"},
63
+ ZWD: {"name":"Zimbabwe Dollar","numeric":"716","exponent":2,"withdrawn":"2008-08"},
64
+ ZWL: {"name":"Zimbabwe Dollar","numeric":"932","exponent":2,"withdrawn":"2024-09"},
65
+ ZWN: {"name":"Zimbabwe Dollar (new)","numeric":"942","exponent":2,"withdrawn":"2006-09"},
66
+ ZWR: {"name":"Zimbabwe Dollar","numeric":"935","exponent":2,"withdrawn":"2009-06"},
67
+ } as const;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `@ducatjs/money/historic`: withdrawn ISO 4217 codes and a few non-ISO ones, with the exponent
3
+ * each had and, for ISO codes, the month ISO withdrew it. Nothing is registered until you ask, so
4
+ * the core registry stays ISO 4217 as currently published:
5
+ *
6
+ * defineHistoricCurrencies(['HRK', 'BGN']);
7
+ * money('100', 'HRK'); // 100.00 HRK
8
+ * historicCurrency('HRK')?.withdrawn; // '2023-01'
9
+ */
10
+ import { defineCurrency } from './currency.js';
11
+ import { HISTORIC_CURRENCY_TABLE, ISO_4217_LIST_THREE_PUBLISHED } from './generated/historic.js';
12
+
13
+ export { ISO_4217_LIST_THREE_PUBLISHED };
14
+
15
+ /** A code in the historic table. */
16
+ export type HistoricCurrencyCode = keyof typeof HISTORIC_CURRENCY_TABLE;
17
+
18
+ /**
19
+ * Every historic code, for the `CurrencyCode` type:
20
+ *
21
+ * declare module '@ducatjs/money' {
22
+ * interface CustomCurrencies extends HistoricCurrencies {}
23
+ * }
24
+ */
25
+ export type HistoricCurrencies = { readonly [Code in HistoricCurrencyCode]: true };
26
+
27
+ /** One entry of the historic table. */
28
+ export interface HistoricCurrency {
29
+ readonly code: HistoricCurrencyCode;
30
+ /** The name ISO 4217 List Three gives it (for non-ISO codes, a descriptive name). */
31
+ readonly name: string;
32
+ /** Its minor-unit exponent while it was current: what stored minor units of it mean. */
33
+ readonly exponent: number;
34
+ /** The ISO 4217 numeric code; absent for codes that were never ISO (CNH). */
35
+ readonly numeric?: string;
36
+ /**
37
+ * The year and month ISO 4217 withdrew it ('2023-01'), from List Three. A code withdrawn
38
+ * country by country has the last date. Absent for codes that were never ISO.
39
+ */
40
+ readonly withdrawn?: string;
41
+ }
42
+
43
+ const table: Readonly<Record<string, Omit<HistoricCurrency, 'code'>>> = HISTORIC_CURRENCY_TABLE;
44
+
45
+ /** The table entry for a code, or undefined for a code that isn't in it. */
46
+ export function historicCurrency(code: string): HistoricCurrency | undefined {
47
+ // own properties only, so 'toString' and friends are not currencies
48
+ if (!Object.hasOwn(table, code)) {
49
+ return undefined;
50
+ }
51
+ return { code: code as HistoricCurrencyCode, ...table[code]! };
52
+ }
53
+
54
+ /** Every entry of the table, by code. */
55
+ export function historicCurrencies(): HistoricCurrency[] {
56
+ return Object.keys(table).map((code) => historicCurrency(code)!);
57
+ }
58
+
59
+ /**
60
+ * Registers historic codes with `defineCurrency`, so Money can hold them: all of them, or only
61
+ * `codes`. Registering one again is harmless; a code already defined with another exponent throws,
62
+ * as `defineCurrency` does.
63
+ */
64
+ export function defineHistoricCurrencies(codes?: readonly HistoricCurrencyCode[]): void {
65
+ const selected: readonly string[] = codes ?? Object.keys(table);
66
+
67
+ // check every code first, so a typo registers nothing
68
+ for (const code of selected) {
69
+ if (!Object.hasOwn(table, code)) {
70
+ throw new RangeError(`${code} is not in the historic currency table`);
71
+ }
72
+ }
73
+
74
+ for (const code of selected) {
75
+ defineCurrency(code, table[code]!.exponent);
76
+ }
77
+ }