@ducatjs/money 0.0.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 (398) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +51 -0
  3. package/README.md +1034 -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 +520 -0
  9. package/data/checkout.json +230 -0
  10. package/data/cldr-cash.json +101 -0
  11. package/data/historic.json +78 -0
  12. package/data/iso4217-list-one.xml +1956 -0
  13. package/data/iso4217-list-three.xml +1185 -0
  14. package/data/klarna.json +95 -0
  15. package/data/paypal.json +65 -0
  16. package/data/stripe-fx.json +17 -0
  17. package/data/stripe.json +285 -0
  18. package/dist/adyen-fx.d.ts +25 -0
  19. package/dist/adyen-fx.d.ts.map +1 -0
  20. package/dist/adyen-fx.js +140 -0
  21. package/dist/adyen-fx.js.map +1 -0
  22. package/dist/adyen.d.ts +25 -0
  23. package/dist/adyen.d.ts.map +1 -0
  24. package/dist/adyen.js +34 -0
  25. package/dist/adyen.js.map +1 -0
  26. package/dist/allocate.d.ts +72 -0
  27. package/dist/allocate.d.ts.map +1 -0
  28. package/dist/allocate.js +314 -0
  29. package/dist/allocate.js.map +1 -0
  30. package/dist/bag.d.ts +58 -0
  31. package/dist/bag.d.ts.map +1 -0
  32. package/dist/bag.js +122 -0
  33. package/dist/bag.js.map +1 -0
  34. package/dist/boc.d.ts +31 -0
  35. package/dist/boc.d.ts.map +1 -0
  36. package/dist/boc.js +114 -0
  37. package/dist/boc.js.map +1 -0
  38. package/dist/boe.d.ts +33 -0
  39. package/dist/boe.d.ts.map +1 -0
  40. package/dist/boe.js +114 -0
  41. package/dist/boe.js.map +1 -0
  42. package/dist/braintree.d.ts +34 -0
  43. package/dist/braintree.d.ts.map +1 -0
  44. package/dist/braintree.js +45 -0
  45. package/dist/braintree.js.map +1 -0
  46. package/dist/cash.d.ts +64 -0
  47. package/dist/cash.d.ts.map +1 -0
  48. package/dist/cash.js +134 -0
  49. package/dist/cash.js.map +1 -0
  50. package/dist/checkout.d.ts +42 -0
  51. package/dist/checkout.d.ts.map +1 -0
  52. package/dist/checkout.js +52 -0
  53. package/dist/checkout.js.map +1 -0
  54. package/dist/convert.d.ts +52 -0
  55. package/dist/convert.d.ts.map +1 -0
  56. package/dist/convert.js +65 -0
  57. package/dist/convert.js.map +1 -0
  58. package/dist/currency.d.ts +22 -0
  59. package/dist/currency.d.ts.map +1 -0
  60. package/dist/currency.js +38 -0
  61. package/dist/currency.js.map +1 -0
  62. package/dist/decimal.d.ts +52 -0
  63. package/dist/decimal.d.ts.map +1 -0
  64. package/dist/decimal.js +251 -0
  65. package/dist/decimal.js.map +1 -0
  66. package/dist/drizzle/mysql.d.ts +105 -0
  67. package/dist/drizzle/mysql.d.ts.map +1 -0
  68. package/dist/drizzle/mysql.js +60 -0
  69. package/dist/drizzle/mysql.js.map +1 -0
  70. package/dist/drizzle/pg.d.ts +103 -0
  71. package/dist/drizzle/pg.d.ts.map +1 -0
  72. package/dist/drizzle/pg.js +60 -0
  73. package/dist/drizzle/pg.js.map +1 -0
  74. package/dist/drizzle/sqlite.d.ts +102 -0
  75. package/dist/drizzle/sqlite.d.ts.map +1 -0
  76. package/dist/drizzle/sqlite.js +59 -0
  77. package/dist/drizzle/sqlite.js.map +1 -0
  78. package/dist/ecb.d.ts +52 -0
  79. package/dist/ecb.d.ts.map +1 -0
  80. package/dist/ecb.js +285 -0
  81. package/dist/ecb.js.map +1 -0
  82. package/dist/exact.d.ts +62 -0
  83. package/dist/exact.d.ts.map +1 -0
  84. package/dist/exact.js +147 -0
  85. package/dist/exact.js.map +1 -0
  86. package/dist/fed.d.ts +35 -0
  87. package/dist/fed.d.ts.map +1 -0
  88. package/dist/fed.js +134 -0
  89. package/dist/fed.js.map +1 -0
  90. package/dist/fetch.d.ts +103 -0
  91. package/dist/fetch.d.ts.map +1 -0
  92. package/dist/fetch.js +222 -0
  93. package/dist/fetch.js.map +1 -0
  94. package/dist/fixed.d.ts +121 -0
  95. package/dist/fixed.d.ts.map +1 -0
  96. package/dist/fixed.js +420 -0
  97. package/dist/fixed.js.map +1 -0
  98. package/dist/fn.d.ts +34 -0
  99. package/dist/fn.d.ts.map +1 -0
  100. package/dist/fn.js +37 -0
  101. package/dist/fn.js.map +1 -0
  102. package/dist/format.d.ts +13 -0
  103. package/dist/format.d.ts.map +1 -0
  104. package/dist/format.js +90 -0
  105. package/dist/format.js.map +1 -0
  106. package/dist/generated/adyen.d.ts +3 -0
  107. package/dist/generated/adyen.d.ts.map +1 -0
  108. package/dist/generated/adyen.js +144 -0
  109. package/dist/generated/adyen.js.map +1 -0
  110. package/dist/generated/braintree.d.ts +3 -0
  111. package/dist/generated/braintree.d.ts.map +1 -0
  112. package/dist/generated/braintree.js +140 -0
  113. package/dist/generated/braintree.js.map +1 -0
  114. package/dist/generated/cash.d.ts +14 -0
  115. package/dist/generated/cash.d.ts.map +1 -0
  116. package/dist/generated/cash.js +465 -0
  117. package/dist/generated/cash.js.map +1 -0
  118. package/dist/generated/checkout.d.ts +3 -0
  119. package/dist/generated/checkout.d.ts.map +1 -0
  120. package/dist/generated/checkout.js +151 -0
  121. package/dist/generated/checkout.js.map +1 -0
  122. package/dist/generated/historic.d.ts +337 -0
  123. package/dist/generated/historic.d.ts.map +1 -0
  124. package/dist/generated/historic.js +66 -0
  125. package/dist/generated/historic.js.map +1 -0
  126. package/dist/generated/iso4217.d.ts +174 -0
  127. package/dist/generated/iso4217.d.ts.map +1 -0
  128. package/dist/generated/iso4217.js +175 -0
  129. package/dist/generated/iso4217.js.map +1 -0
  130. package/dist/generated/kernel.d.ts +17 -0
  131. package/dist/generated/kernel.d.ts.map +1 -0
  132. package/dist/generated/kernel.js +208 -0
  133. package/dist/generated/kernel.js.map +1 -0
  134. package/dist/generated/klarna.d.ts +3 -0
  135. package/dist/generated/klarna.d.ts.map +1 -0
  136. package/dist/generated/klarna.js +21 -0
  137. package/dist/generated/klarna.js.map +1 -0
  138. package/dist/generated/paypal.d.ts +3 -0
  139. package/dist/generated/paypal.d.ts.map +1 -0
  140. package/dist/generated/paypal.js +31 -0
  141. package/dist/generated/paypal.js.map +1 -0
  142. package/dist/generated/stripe.d.ts +8 -0
  143. package/dist/generated/stripe.d.ts.map +1 -0
  144. package/dist/generated/stripe.js +150 -0
  145. package/dist/generated/stripe.js.map +1 -0
  146. package/dist/historic.d.ts +40 -0
  147. package/dist/historic.d.ts.map +1 -0
  148. package/dist/historic.js +43 -0
  149. package/dist/historic.js.map +1 -0
  150. package/dist/history.d.ts +96 -0
  151. package/dist/history.d.ts.map +1 -0
  152. package/dist/history.js +213 -0
  153. package/dist/history.js.map +1 -0
  154. package/dist/hmrc.d.ts +28 -0
  155. package/dist/hmrc.d.ts.map +1 -0
  156. package/dist/hmrc.js +108 -0
  157. package/dist/hmrc.js.map +1 -0
  158. package/dist/index.d.ts +11 -0
  159. package/dist/index.d.ts.map +1 -0
  160. package/dist/index.js +11 -0
  161. package/dist/index.js.map +1 -0
  162. package/dist/klarna.d.ts +31 -0
  163. package/dist/klarna.d.ts.map +1 -0
  164. package/dist/klarna.js +38 -0
  165. package/dist/klarna.js.map +1 -0
  166. package/dist/kysely.d.ts +120 -0
  167. package/dist/kysely.d.ts.map +1 -0
  168. package/dist/kysely.js +309 -0
  169. package/dist/kysely.js.map +1 -0
  170. package/dist/money.d.ts +302 -0
  171. package/dist/money.d.ts.map +1 -0
  172. package/dist/money.js +703 -0
  173. package/dist/money.js.map +1 -0
  174. package/dist/parse.d.ts +55 -0
  175. package/dist/parse.d.ts.map +1 -0
  176. package/dist/parse.js +419 -0
  177. package/dist/parse.js.map +1 -0
  178. package/dist/paypal.d.ts +41 -0
  179. package/dist/paypal.d.ts.map +1 -0
  180. package/dist/paypal.js +58 -0
  181. package/dist/paypal.js.map +1 -0
  182. package/dist/pg.d.ts +100 -0
  183. package/dist/pg.d.ts.map +1 -0
  184. package/dist/pg.js +343 -0
  185. package/dist/pg.js.map +1 -0
  186. package/dist/plugins/adyen.d.ts +15 -0
  187. package/dist/plugins/adyen.d.ts.map +1 -0
  188. package/dist/plugins/adyen.js +10 -0
  189. package/dist/plugins/adyen.js.map +1 -0
  190. package/dist/plugins/all.d.ts +29 -0
  191. package/dist/plugins/all.d.ts.map +1 -0
  192. package/dist/plugins/all.js +30 -0
  193. package/dist/plugins/all.js.map +1 -0
  194. package/dist/plugins/allocate.d.ts +20 -0
  195. package/dist/plugins/allocate.d.ts.map +1 -0
  196. package/dist/plugins/allocate.js +26 -0
  197. package/dist/plugins/allocate.js.map +1 -0
  198. package/dist/plugins/braintree.d.ts +12 -0
  199. package/dist/plugins/braintree.d.ts.map +1 -0
  200. package/dist/plugins/braintree.js +12 -0
  201. package/dist/plugins/braintree.js.map +1 -0
  202. package/dist/plugins/cash.d.ts +19 -0
  203. package/dist/plugins/cash.d.ts.map +1 -0
  204. package/dist/plugins/cash.js +16 -0
  205. package/dist/plugins/cash.js.map +1 -0
  206. package/dist/plugins/checkout.d.ts +21 -0
  207. package/dist/plugins/checkout.d.ts.map +1 -0
  208. package/dist/plugins/checkout.js +16 -0
  209. package/dist/plugins/checkout.js.map +1 -0
  210. package/dist/plugins/convert.d.ts +19 -0
  211. package/dist/plugins/convert.d.ts.map +1 -0
  212. package/dist/plugins/convert.js +13 -0
  213. package/dist/plugins/convert.js.map +1 -0
  214. package/dist/plugins/exact.d.ts +15 -0
  215. package/dist/plugins/exact.d.ts.map +1 -0
  216. package/dist/plugins/exact.js +13 -0
  217. package/dist/plugins/exact.js.map +1 -0
  218. package/dist/plugins/fixed.d.ts +17 -0
  219. package/dist/plugins/fixed.d.ts.map +1 -0
  220. package/dist/plugins/fixed.js +13 -0
  221. package/dist/plugins/fixed.js.map +1 -0
  222. package/dist/plugins/format.d.ts +9 -0
  223. package/dist/plugins/format.d.ts.map +1 -0
  224. package/dist/plugins/format.js +16 -0
  225. package/dist/plugins/format.js.map +1 -0
  226. package/dist/plugins/klarna.d.ts +10 -0
  227. package/dist/plugins/klarna.d.ts.map +1 -0
  228. package/dist/plugins/klarna.js +10 -0
  229. package/dist/plugins/klarna.js.map +1 -0
  230. package/dist/plugins/paypal.d.ts +14 -0
  231. package/dist/plugins/paypal.d.ts.map +1 -0
  232. package/dist/plugins/paypal.js +16 -0
  233. package/dist/plugins/paypal.js.map +1 -0
  234. package/dist/plugins/prisma.d.ts +8 -0
  235. package/dist/plugins/prisma.d.ts.map +1 -0
  236. package/dist/plugins/prisma.js +13 -0
  237. package/dist/plugins/prisma.js.map +1 -0
  238. package/dist/plugins/stripe.d.ts +14 -0
  239. package/dist/plugins/stripe.d.ts.map +1 -0
  240. package/dist/plugins/stripe.js +16 -0
  241. package/dist/plugins/stripe.js.map +1 -0
  242. package/dist/plugins/tax.d.ts +20 -0
  243. package/dist/plugins/tax.d.ts.map +1 -0
  244. package/dist/plugins/tax.js +18 -0
  245. package/dist/plugins/tax.js.map +1 -0
  246. package/dist/policy.d.ts +34 -0
  247. package/dist/policy.d.ts.map +1 -0
  248. package/dist/policy.js +90 -0
  249. package/dist/policy.js.map +1 -0
  250. package/dist/prisma.d.ts +77 -0
  251. package/dist/prisma.d.ts.map +1 -0
  252. package/dist/prisma.js +76 -0
  253. package/dist/prisma.js.map +1 -0
  254. package/dist/processor.d.ts +28 -0
  255. package/dist/processor.d.ts.map +1 -0
  256. package/dist/processor.js +118 -0
  257. package/dist/processor.js.map +1 -0
  258. package/dist/provider.d.ts +103 -0
  259. package/dist/provider.d.ts.map +1 -0
  260. package/dist/provider.js +280 -0
  261. package/dist/provider.js.map +1 -0
  262. package/dist/rate-text.d.ts +38 -0
  263. package/dist/rate-text.d.ts.map +1 -0
  264. package/dist/rate-text.js +143 -0
  265. package/dist/rate-text.js.map +1 -0
  266. package/dist/rate.d.ts +175 -0
  267. package/dist/rate.d.ts.map +1 -0
  268. package/dist/rate.js +371 -0
  269. package/dist/rate.js.map +1 -0
  270. package/dist/rounding-error.d.ts +9 -0
  271. package/dist/rounding-error.d.ts.map +1 -0
  272. package/dist/rounding-error.js +9 -0
  273. package/dist/rounding-error.js.map +1 -0
  274. package/dist/rounding.d.ts +50 -0
  275. package/dist/rounding.d.ts.map +1 -0
  276. package/dist/rounding.js +87 -0
  277. package/dist/rounding.js.map +1 -0
  278. package/dist/schema.d.ts +43 -0
  279. package/dist/schema.d.ts.map +1 -0
  280. package/dist/schema.js +77 -0
  281. package/dist/schema.js.map +1 -0
  282. package/dist/sql.d.ts +54 -0
  283. package/dist/sql.d.ts.map +1 -0
  284. package/dist/sql.js +131 -0
  285. package/dist/sql.js.map +1 -0
  286. package/dist/stripe-fx.d.ts +22 -0
  287. package/dist/stripe-fx.d.ts.map +1 -0
  288. package/dist/stripe-fx.js +30 -0
  289. package/dist/stripe-fx.js.map +1 -0
  290. package/dist/stripe.d.ts +36 -0
  291. package/dist/stripe.d.ts.map +1 -0
  292. package/dist/stripe.js +39 -0
  293. package/dist/stripe.js.map +1 -0
  294. package/dist/tax.d.ts +33 -0
  295. package/dist/tax.d.ts.map +1 -0
  296. package/dist/tax.js +55 -0
  297. package/dist/tax.js.map +1 -0
  298. package/dist/testing/arbitraries.d.ts +43 -0
  299. package/dist/testing/arbitraries.d.ts.map +1 -0
  300. package/dist/testing/arbitraries.js +98 -0
  301. package/dist/testing/arbitraries.js.map +1 -0
  302. package/dist/testing/index.d.ts +67 -0
  303. package/dist/testing/index.d.ts.map +1 -0
  304. package/dist/testing/index.js +159 -0
  305. package/dist/testing/index.js.map +1 -0
  306. package/dist/testing/jest.d.ts +2 -0
  307. package/dist/testing/jest.d.ts.map +1 -0
  308. package/dist/testing/jest.js +15 -0
  309. package/dist/testing/jest.js.map +1 -0
  310. package/dist/testing/vitest.d.ts +7 -0
  311. package/dist/testing/vitest.d.ts.map +1 -0
  312. package/dist/testing/vitest.js +12 -0
  313. package/dist/testing/vitest.js.map +1 -0
  314. package/dist/valibot.d.ts +33 -0
  315. package/dist/valibot.d.ts.map +1 -0
  316. package/dist/valibot.js +61 -0
  317. package/dist/valibot.js.map +1 -0
  318. package/dist/zod.d.ts +36 -0
  319. package/dist/zod.d.ts.map +1 -0
  320. package/dist/zod.js +58 -0
  321. package/dist/zod.js.map +1 -0
  322. package/package.json +280 -5
  323. package/src/adyen-fx.ts +175 -0
  324. package/src/adyen.ts +46 -0
  325. package/src/allocate.ts +393 -0
  326. package/src/bag.ts +170 -0
  327. package/src/boc.ts +136 -0
  328. package/src/boe.ts +131 -0
  329. package/src/braintree.ts +71 -0
  330. package/src/cash.ts +183 -0
  331. package/src/checkout.ts +77 -0
  332. package/src/convert.ts +177 -0
  333. package/src/currency.ts +61 -0
  334. package/src/decimal.ts +283 -0
  335. package/src/drizzle/mysql.ts +78 -0
  336. package/src/drizzle/pg.ts +78 -0
  337. package/src/drizzle/sqlite.ts +70 -0
  338. package/src/ecb.ts +328 -0
  339. package/src/exact.ts +182 -0
  340. package/src/fed.ts +146 -0
  341. package/src/fetch.ts +306 -0
  342. package/src/fixed.ts +558 -0
  343. package/src/fn.ts +94 -0
  344. package/src/format.ts +106 -0
  345. package/src/generated/adyen.ts +144 -0
  346. package/src/generated/braintree.ts +140 -0
  347. package/src/generated/cash.ts +478 -0
  348. package/src/generated/checkout.ts +151 -0
  349. package/src/generated/historic.ts +67 -0
  350. package/src/generated/iso4217.ts +179 -0
  351. package/src/generated/kernel.ts +230 -0
  352. package/src/generated/klarna.ts +21 -0
  353. package/src/generated/paypal.ts +31 -0
  354. package/src/generated/stripe.ts +150 -0
  355. package/src/historic.ts +77 -0
  356. package/src/history.ts +333 -0
  357. package/src/hmrc.ts +125 -0
  358. package/src/index.ts +41 -0
  359. package/src/klarna.ts +54 -0
  360. package/src/kysely.ts +588 -0
  361. package/src/money.ts +979 -0
  362. package/src/parse.ts +511 -0
  363. package/src/paypal.ts +93 -0
  364. package/src/pg.ts +410 -0
  365. package/src/plugins/adyen.ts +25 -0
  366. package/src/plugins/all.ts +57 -0
  367. package/src/plugins/allocate.ts +53 -0
  368. package/src/plugins/braintree.ts +23 -0
  369. package/src/plugins/cash.ts +33 -0
  370. package/src/plugins/checkout.ts +32 -0
  371. package/src/plugins/convert.ts +49 -0
  372. package/src/plugins/exact.ts +23 -0
  373. package/src/plugins/fixed.ts +28 -0
  374. package/src/plugins/format.ts +28 -0
  375. package/src/plugins/klarna.ts +21 -0
  376. package/src/plugins/paypal.ts +29 -0
  377. package/src/plugins/prisma.ts +24 -0
  378. package/src/plugins/stripe.ts +29 -0
  379. package/src/plugins/tax.ts +40 -0
  380. package/src/policy.ts +124 -0
  381. package/src/prisma.ts +199 -0
  382. package/src/processor.ts +181 -0
  383. package/src/provider.ts +389 -0
  384. package/src/rate-text.ts +151 -0
  385. package/src/rate.ts +546 -0
  386. package/src/rounding-error.ts +8 -0
  387. package/src/rounding.ts +122 -0
  388. package/src/schema.ts +119 -0
  389. package/src/sql.ts +178 -0
  390. package/src/stripe-fx.ts +49 -0
  391. package/src/stripe.ts +69 -0
  392. package/src/tax.ts +102 -0
  393. package/src/testing/arbitraries.ts +134 -0
  394. package/src/testing/index.ts +247 -0
  395. package/src/testing/jest.ts +18 -0
  396. package/src/testing/vitest.ts +18 -0
  397. package/src/valibot.ts +87 -0
  398. package/src/zod.ts +78 -0
package/src/parse.ts ADDED
@@ -0,0 +1,511 @@
1
+ import { ISO_EXPONENTS, ISO_NON_MONETARY } from './currency.js';
2
+ import { money, type CurrencyInput, type Money, type MoneyOptions } from './money.js';
3
+
4
+ interface Symbols {
5
+ group: string;
6
+ decimal: string;
7
+ /** The locale's group sizes, looked up the first time a group separator is seen. */
8
+ groupSizes?: readonly GroupSizes[];
9
+ }
10
+ const symCache = new Map<string, Symbols>();
11
+
12
+ /**
13
+ * The group and decimal separators `format()` uses for this locale and currency. They come from a
14
+ * currency-style formatter, not a plain number one, because some locales use different separators
15
+ * for money: fr-CH writes 1 234.56 € but 1 234,56 for a number, and de-AT 1.234,56 € but 1 234,56.
16
+ */
17
+ function symbols(locale: string | undefined, currency: string): Symbols {
18
+ const key = `${locale ?? ''}|${currency}`;
19
+ let found = symCache.get(key);
20
+ if (!found) {
21
+ const parts = separatorParts(locale, currency);
22
+ found = {
23
+ group: parts.find((part) => part.type === 'group')?.value ?? ',',
24
+ decimal: parts.find((part) => part.type === 'decimal')?.value ?? '.',
25
+ };
26
+ if (symCache.size > 256) {
27
+ symCache.clear();
28
+ }
29
+ symCache.set(key, found);
30
+ }
31
+ return found;
32
+ }
33
+
34
+ function separatorParts(
35
+ locale: string | undefined,
36
+ currency: string,
37
+ currencySign: 'standard' | 'accounting' = 'standard',
38
+ ): Intl.NumberFormatPart[] {
39
+ try {
40
+ // Two fraction digits even for JPY and the like, so the decimal separator always appears.
41
+ return new Intl.NumberFormat(locale, {
42
+ style: 'currency',
43
+ currency,
44
+ currencySign,
45
+ useGrouping: true,
46
+ minimumFractionDigits: 2,
47
+ maximumFractionDigits: 2,
48
+ }).formatToParts(1234567.5);
49
+ } catch {
50
+ // Codes Intl rejects as currencies (custom ones that are not three letters): a number format's
51
+ // separators.
52
+ return new Intl.NumberFormat(locale, { useGrouping: true }).formatToParts(1234567.5);
53
+ }
54
+ }
55
+
56
+ // Zero code points of the decimal digit blocks Intl uses (arab, arabext, deva, beng, thai,
57
+ // fullwide, ...).
58
+ const ZEROS = [
59
+ 0x660, 0x6f0, 0x7c0, 0x966, 0x9e6, 0xa66, 0xae6, 0xb66, 0xbe6, 0xc66, 0xce6, 0xd66, 0xe50, 0xed0,
60
+ 0xf20, 0x1040, 0x17e0, 0x1810, 0xff10,
61
+ ];
62
+ const FULLWIDTH_ZERO = 0xff10;
63
+ const LAST_BLOCK_BEFORE_FULLWIDTH = 0x1810;
64
+ function asciiDigit(cp: number): number {
65
+ if (cp >= 48 && cp <= 57) {
66
+ return cp - 48;
67
+ }
68
+ // Every non-digit character used to scan the whole table. Separators, signs and Latin-1 spaces
69
+ // are below the first block (U+0660); currency signs such as € (U+20AC) and the narrow no-break
70
+ // space (U+202F) fall in the gap before the fullwidth digits.
71
+ if (cp < ZEROS[0]! || (cp > LAST_BLOCK_BEFORE_FULLWIDTH + 9 && cp < FULLWIDTH_ZERO)) {
72
+ return -1;
73
+ }
74
+ // ZEROS is sorted, so the first block above cp ends the search
75
+ for (let i = 0; i < ZEROS.length; i++) {
76
+ const zero = ZEROS[i]!;
77
+ if (cp < zero) {
78
+ return -1;
79
+ }
80
+ if (cp <= zero + 9) {
81
+ return cp - zero;
82
+ }
83
+ }
84
+ return -1;
85
+ }
86
+
87
+ /**
88
+ * Parses a locale-formatted amount: parse('1.234,50 €', 'EUR', 'de-DE'), parse('CHF 1’234.50',
89
+ * 'CHF', 'de-CH'), parse('١٢٣٫٤٥ US$', 'USD', 'ar-EG'). Currency symbols/codes, spaces and bidi
90
+ * marks around the number are ignored (the currency is the `currency` argument, never guessed from
91
+ * the text). A minus sign (ASCII or U+2212) or accounting parentheses anywhere in the affixes make
92
+ * it negative. Inside the number only digits, the locale's group separator (before the decimal
93
+ * separator) and one decimal separator are allowed.
94
+ *
95
+ * Group separators must be where the locale puts them (`1.234,50` in de-DE, `1,23,456.78` in
96
+ * en-IN), because a misplaced one is usually a decimal separator from another locale: `78.00` in
97
+ * de-DE throws instead of becoming 7800.00. Pass `{ lenient: true }` to accept them anywhere.
98
+ */
99
+ export function parse<C extends CurrencyInput>(
100
+ input: string,
101
+ currency: C,
102
+ locale?: string,
103
+ options?: ParseOptions,
104
+ ): Money<C> {
105
+ const localeSymbols = symbols(locale, currency);
106
+ const { group, decimal } = localeSymbols;
107
+ const chars = Array.from(input);
108
+ let first = -1;
109
+ let last = -1;
110
+ for (let i = 0; i < chars.length; i++) {
111
+ if (asciiDigit(chars[i]!.codePointAt(0)!) >= 0) {
112
+ if (first < 0) {
113
+ first = i;
114
+ }
115
+ last = i;
116
+ }
117
+ }
118
+ if (first < 0) {
119
+ throw new SyntaxError(`Cannot parse "${input}": no digits`);
120
+ }
121
+ const prefix = chars.slice(0, first).join('');
122
+ const suffix = chars.slice(last + 1).join('');
123
+ const affix = prefix + suffix;
124
+ if (!/^[\p{L}\p{Sc}\p{Z}\s\p{Cf}.()+\-−]*$/u.test(affix)) {
125
+ throw new SyntaxError(`Cannot parse "${input}": unexpected characters`);
126
+ }
127
+ const neg = /[-−(]/.test(affix);
128
+ const isGroup = (ch: string) =>
129
+ ch === group ||
130
+ (/^[\s\p{Z}]$/u.test(group) && /^[\s\p{Z}]$/u.test(ch)) ||
131
+ ((group === '’' || group === "'") && (ch === "'" || ch === '’')) ||
132
+ ch === '٬';
133
+ let out = prefix.endsWith(decimal) ? '0.' : '';
134
+ let seenDecimal = out !== '';
135
+ // The integer part's digit runs between group separators, as counters (most input has none, and
136
+ // then nothing is checked): the first run, the length every middle run shares, and the last run.
137
+ let separators = 0;
138
+ let firstRun = 0;
139
+ let middleRun = -1;
140
+ let middleRunsAgree = true;
141
+ let lastRun = -1;
142
+ let digitsInGroup = 0;
143
+ for (let i = first; i <= last; i++) {
144
+ const ch = chars[i]!;
145
+ const digit = asciiDigit(ch.codePointAt(0)!);
146
+ if (digit >= 0) {
147
+ out += digit;
148
+ digitsInGroup++;
149
+ } else if (ch === decimal || ch === '٫') {
150
+ if (seenDecimal) {
151
+ throw new SyntaxError(`Cannot parse "${input}": more than one decimal separator`);
152
+ }
153
+ seenDecimal = true;
154
+ out += '.';
155
+ lastRun = digitsInGroup;
156
+ } else if (isGroup(ch)) {
157
+ if (seenDecimal) {
158
+ throw new SyntaxError(
159
+ `Cannot parse "${input}": group separator after the decimal separator`,
160
+ );
161
+ }
162
+ if (separators === 0) {
163
+ firstRun = digitsInGroup;
164
+ } else if (middleRun < 0) {
165
+ middleRun = digitsInGroup;
166
+ } else if (middleRun !== digitsInGroup) {
167
+ middleRunsAgree = false;
168
+ }
169
+ separators++;
170
+ digitsInGroup = 0;
171
+ } else {
172
+ throw new SyntaxError(`Cannot parse "${input}": unexpected "${ch}"`);
173
+ }
174
+ }
175
+ if (separators > 0 && options?.lenient !== true) {
176
+ if (lastRun < 0) {
177
+ lastRun = digitsInGroup;
178
+ }
179
+ localeSymbols.groupSizes ??= groupSizes(locale, currency);
180
+ checkRuns(input, localeSymbols.groupSizes, firstRun, middleRunsAgree ? middleRun : -2, lastRun);
181
+ }
182
+ // pass options through as they are unless they carry `lenient`, so the common call allocates nothing
183
+ let moneyOptions: MoneyOptions | undefined = options;
184
+ if (options !== undefined && 'lenient' in options) {
185
+ const { lenient: _lenient, ...rest } = options;
186
+ moneyOptions = rest;
187
+ }
188
+ return money((neg ? '-' : '') + out, currency, moneyOptions);
189
+ }
190
+
191
+ /** Options for `parse`. */
192
+ export interface ParseOptions extends MoneyOptions {
193
+ /** Accept group separators in any position, as before 0.2 (`1.5` in de-DE is then 15.00). */
194
+ readonly lenient?: boolean;
195
+ }
196
+
197
+ /** Options for `parseMoney`. */
198
+ export interface ParseMoneyOptions<C extends string = string> extends MoneyOptions {
199
+ /**
200
+ * The expected currency. It settles a symbol several currencies share (`$`, `kr`, `£`, `¥`), and
201
+ * a text naming another currency is an error. With `lenient`, a bare number is read in it.
202
+ */
203
+ readonly hint?: C;
204
+ /**
205
+ * Your own meanings for symbols, which win over the locale's: `{ $: 'CAD' }` for a shop that only
206
+ * prices in Canadian dollars.
207
+ */
208
+ readonly symbols?: Readonly<Record<string, C>>;
209
+ /** The currencies to detect (default: every ISO 4217 currency). Others are not recognised. */
210
+ readonly currencies?: readonly C[];
211
+ /**
212
+ * Accept group separators in any positions (as `parse` does with `lenient`), and a bare number when a `hint`
213
+ * is given. It never makes the currency a guess.
214
+ */
215
+ readonly lenient?: boolean;
216
+ }
217
+
218
+ const symbolTables = new Map<string, Map<string, Set<string>>>();
219
+ const groupSizeCache = new Map<string, GroupSizes[]>();
220
+
221
+ /** The size of the last group, and of the groups before it: [3, 3] for 1,234,567, [3, 2] for 12,34,567. */
222
+ type GroupSizes = readonly [number, number];
223
+ const supportedLocales = new Map<string, boolean>();
224
+
225
+ /** Strips what surrounds a symbol in formatted output: spaces, no-break spaces and bidi marks. */
226
+ const normalizeSymbol = (text: string) => text.replace(/[\s\p{Z}\p{Cf}]/gu, '');
227
+
228
+ function currencySymbol(
229
+ locale: string,
230
+ currency: string,
231
+ display: 'symbol' | 'narrowSymbol',
232
+ ): string | undefined {
233
+ try {
234
+ const parts = new Intl.NumberFormat(locale, {
235
+ style: 'currency',
236
+ currency,
237
+ currencyDisplay: display,
238
+ }).formatToParts(1);
239
+ const value = parts.find((part) => part.type === 'currency')?.value;
240
+ return value === undefined ? undefined : normalizeSymbol(value);
241
+ } catch {
242
+ return undefined;
243
+ }
244
+ }
245
+
246
+ /** Language and region of a locale tag, the part Intl must have data for. */
247
+ function languageAndRegion(tag: string): string {
248
+ const parsed = new Intl.Locale(tag);
249
+ return `${parsed.language}-${parsed.region ?? ''}`;
250
+ }
251
+
252
+ /**
253
+ * Throws unless Intl has data for the locale's language and region. Intl silently falls back to
254
+ * another locale (an unsupported tag, or a runtime with reduced ICU data), and that locale's
255
+ * separators and symbols would then decide the amount and the currency.
256
+ */
257
+ function requireLocaleData(input: string, locale: string): void {
258
+ let isSupported = supportedLocales.get(locale);
259
+ if (isSupported === undefined) {
260
+ const resolved = new Intl.NumberFormat(locale).resolvedOptions().locale;
261
+ isSupported = languageAndRegion(resolved) === languageAndRegion(locale);
262
+ if (supportedLocales.size >= 64) {
263
+ supportedLocales.clear();
264
+ }
265
+ supportedLocales.set(locale, isSupported);
266
+ }
267
+ if (!isSupported) {
268
+ throw new RangeError(
269
+ `Cannot parse "${input}": this runtime has no Intl data for the locale ${locale}`,
270
+ );
271
+ }
272
+ }
273
+
274
+ /**
275
+ * symbol → every currency it can mean in one locale (symbol and narrow symbol), from Intl's own
276
+ * data, so it follows the runtime's CLDR version. Built on first use, cached per locale (at most
277
+ * 16), and narrowed by a `currencies` option rather than rebuilt.
278
+ */
279
+ function symbolTable(locale: string): Map<string, Set<string>> {
280
+ let table = symbolTables.get(locale);
281
+ if (table !== undefined) {
282
+ return table;
283
+ }
284
+ table = new Map();
285
+ for (const currency of defaultCandidates()) {
286
+ for (const display of ['symbol', 'narrowSymbol'] as const) {
287
+ const symbol = currencySymbol(locale, currency, display);
288
+ // A symbol that is just the code is handled as a code.
289
+ if (symbol !== undefined && symbol !== currency) {
290
+ let meanings = table.get(symbol);
291
+ if (meanings === undefined) {
292
+ meanings = new Set();
293
+ table.set(symbol, meanings);
294
+ }
295
+ meanings.add(currency);
296
+ }
297
+ }
298
+ }
299
+ if (symbolTables.size >= 16) {
300
+ symbolTables.clear();
301
+ }
302
+ symbolTables.set(locale, table);
303
+ return table;
304
+ }
305
+
306
+ /** Where the English symbols come from: en has CA$, A$ and the like, en-CA has US$. */
307
+ const ENGLISH_SYMBOL_LOCALES = ['en', 'en-CA'];
308
+
309
+ let isoCodes: string[] | undefined;
310
+
311
+ /** Every ISO 4217 currency that money is counted in (not gold, test or fund codes). */
312
+ function defaultCandidates(): string[] {
313
+ isoCodes ??= Object.keys(ISO_EXPONENTS).filter(
314
+ (code) => !(ISO_NON_MONETARY as readonly string[]).includes(code),
315
+ );
316
+ return isoCodes;
317
+ }
318
+
319
+ /** The currencies one piece of affix text can mean; an empty list if it isn't a currency. */
320
+ function meaningsOf(
321
+ piece: string,
322
+ locale: string,
323
+ candidates: ReadonlySet<string> | undefined,
324
+ symbols: Readonly<Record<string, string>> | undefined,
325
+ ): string[] {
326
+ const isCandidate = (code: string) =>
327
+ candidates === undefined ? defaultCandidates().includes(code) : candidates.has(code);
328
+ if (/^[A-Z]{3}$/.test(piece) && isCandidate(piece)) {
329
+ return [piece];
330
+ }
331
+ if (symbols !== undefined && Object.hasOwn(symbols, piece)) {
332
+ return [symbols[piece]!];
333
+ }
334
+ // The locale's own symbols first, then English ones (US$, CA$, A$...), which many texts use.
335
+ for (const source of [locale, ...ENGLISH_SYMBOL_LOCALES]) {
336
+ const found = symbolTable(source).get(piece);
337
+ const meanings = found === undefined ? [] : [...found].filter(isCandidate);
338
+ if (meanings.length > 0) {
339
+ return meanings.sort();
340
+ }
341
+ }
342
+ return [];
343
+ }
344
+
345
+ /**
346
+ * The group sizes the locale formats this currency with, in its standard and its accounting
347
+ * format: they can differ (en-IN writes JP¥1,00,000, but (JP¥100,000) in accounting style).
348
+ */
349
+ function groupSizes(locale: string | undefined, currency: string): readonly GroupSizes[] {
350
+ const key = `${locale ?? ''}|${currency}`;
351
+ let sizes = groupSizeCache.get(key);
352
+ if (sizes === undefined) {
353
+ sizes = [];
354
+ for (const currencySign of ['standard', 'accounting'] as const) {
355
+ const found = sizesOf(separatorParts(locale, currency, currencySign));
356
+ if (!sizes.some((known) => known[0] === found[0] && known[1] === found[1])) {
357
+ sizes.push(found);
358
+ }
359
+ }
360
+ if (groupSizeCache.size >= 256) {
361
+ groupSizeCache.clear();
362
+ }
363
+ groupSizeCache.set(key, sizes);
364
+ }
365
+ return sizes;
366
+ }
367
+
368
+ function sizesOf(parts: Intl.NumberFormatPart[]): GroupSizes {
369
+ const integers = parts.filter((part) => part.type === 'integer').map((part) => part.value.length);
370
+ // 1234567.5 formats as 1|234|567 (sizes 3, 3) or 12|34|567 (3, 2)
371
+ const primary = integers[integers.length - 1] ?? 3;
372
+ const secondary = integers.length >= 3 ? integers[integers.length - 2]! : primary;
373
+ return [primary, secondary];
374
+ }
375
+
376
+ /** Throws unless the integer part's groups follow one of the locale's sizes (when it has groups). */
377
+ function checkGroups(input: string, integerPart: string, allSizes: readonly GroupSizes[]): void {
378
+ // the caller has turned every separator into a comma
379
+ const runs = integerPart.split(',');
380
+ if (runs.length <= 1) {
381
+ return;
382
+ }
383
+ const middle = runs.slice(1, -1).map((run) => run.length);
384
+ let middleRun = -1;
385
+ if (middle.length > 0) {
386
+ middleRun = middle.every((length) => length === middle[0]) ? middle[0]! : -2;
387
+ }
388
+ checkRuns(input, allSizes, runs[0]!.length, middleRun, runs[runs.length - 1]!.length);
389
+ }
390
+
391
+ /**
392
+ * Throws unless digit runs between group separators fit one of the sizes: 1,234,567 or 12,34,567
393
+ * has a leading run of 1 up to `secondary` digits, middle runs of `secondary` and a last run of
394
+ * `primary`. `middleRun` is the length all middle runs share, -1 for none, -2 if they differ.
395
+ */
396
+ function checkRuns(
397
+ input: string,
398
+ allSizes: readonly GroupSizes[],
399
+ firstRun: number,
400
+ middleRun: number,
401
+ lastRun: number,
402
+ ): void {
403
+ for (let option = 0; option < allSizes.length; option++) {
404
+ const [primary, secondary] = allSizes[option]!;
405
+ if (
406
+ lastRun === primary &&
407
+ firstRun >= 1 &&
408
+ firstRun <= secondary &&
409
+ (middleRun === -1 || middleRun === secondary)
410
+ ) {
411
+ return;
412
+ }
413
+ }
414
+ throw new SyntaxError(`Cannot parse "${input}": group separators in the wrong places`);
415
+ }
416
+
417
+ /**
418
+ * Parses a formatted amount and detects its currency from the text: `parseMoney('CHF 1’234.50',
419
+ * 'de-CH')`, `parseMoney('1.234,50 €', 'de-DE')`. Like the rest of ducat it never guesses:
420
+ *
421
+ * - it reads ISO codes (uppercase, among `currencies`) and symbols that mean one currency in the
422
+ * locale (or in English: US$, CA$, A$);
423
+ * - a symbol several currencies share (`$`, `kr`, `£`, `¥`) needs a `hint` or a `symbols` entry;
424
+ * - a currency other than the `hint`, two different currencies, or text that isn't a currency
425
+ * throws;
426
+ * - so does a locale this runtime has no Intl data for, and group separators in the wrong places
427
+ * (`lenient: true` accepts those, and a bare number when a `hint` is given).
428
+ *
429
+ * The number is read as `parse` reads it, with the detected currency's separators.
430
+ */
431
+ export function parseMoney<C extends string = string>(
432
+ input: string,
433
+ locale: string,
434
+ options?: ParseMoneyOptions<C>,
435
+ ): Money<C> {
436
+ const hint = options?.hint;
437
+ const lenient = options?.lenient === true;
438
+ const candidates =
439
+ options?.currencies === undefined ? undefined : new Set<string>(options.currencies);
440
+ requireLocaleData(input, locale);
441
+
442
+ // The text around the number: everything before the first digit and after the last.
443
+ const chars = Array.from(input);
444
+ let first = -1;
445
+ let last = -1;
446
+ for (let i = 0; i < chars.length; i++) {
447
+ if (asciiDigit(chars[i]!.codePointAt(0)!) >= 0) {
448
+ if (first < 0) {
449
+ first = i;
450
+ }
451
+ last = i;
452
+ }
453
+ }
454
+ if (first < 0) {
455
+ throw new SyntaxError(`Cannot parse "${input}": no digits`);
456
+ }
457
+ const affix = `${chars.slice(0, first).join('')} ${chars.slice(last + 1).join('')}`;
458
+
459
+ // Each piece of the affix must be a sign or a currency, and every currency must agree. A lone
460
+ // separator next to the number (".50 USD") belongs to the number, which parse reads.
461
+ let detected: string | undefined;
462
+ const pieces = affix
463
+ .split(/[\s\p{Z}\p{Cf}()+\-−]+/u)
464
+ .filter((piece) => piece !== '' && !/^[.,٫]$/u.test(piece));
465
+ for (const piece of pieces) {
466
+ const meanings = meaningsOf(piece, locale, candidates, options?.symbols);
467
+ let currency: string;
468
+ if (meanings.length === 1) {
469
+ currency = meanings[0]!;
470
+ } else if (meanings.length === 0) {
471
+ throw new SyntaxError(`Cannot parse "${input}": "${piece}" is not a currency`);
472
+ } else if (hint !== undefined && meanings.includes(hint)) {
473
+ currency = hint;
474
+ } else {
475
+ throw new SyntaxError(
476
+ `Cannot parse "${input}": "${piece}" could be ${meanings.join(', ')}; pass a hint or symbols`,
477
+ );
478
+ }
479
+ if (detected !== undefined && detected !== currency) {
480
+ throw new SyntaxError(`Cannot parse "${input}": both ${detected} and ${currency}`);
481
+ }
482
+ detected = currency;
483
+ }
484
+
485
+ if (detected === undefined) {
486
+ if (hint === undefined || !lenient) {
487
+ throw new SyntaxError(
488
+ `Cannot parse "${input}": no currency in the text` +
489
+ (hint === undefined ? '' : '; pass lenient: true to read a bare number in the hint'),
490
+ );
491
+ }
492
+ detected = hint;
493
+ } else if (hint !== undefined && detected !== hint) {
494
+ // a text that names another currency than expected is never a correct parse
495
+ throw new SyntaxError(`Cannot parse "${input}": ${detected}, expected ${hint}`);
496
+ }
497
+
498
+ if (!lenient) {
499
+ const { decimal } = symbols(locale, detected);
500
+ const number = chars.slice(first, last + 1).join('');
501
+ const decimalAt = Math.max(number.indexOf(decimal), number.indexOf('٫'));
502
+ const integerPart = decimalAt === -1 ? number : number.slice(0, decimalAt);
503
+ const asciiInteger = Array.from(integerPart, (ch) => {
504
+ const digit = asciiDigit(ch.codePointAt(0)!);
505
+ return digit >= 0 ? String(digit) : ',';
506
+ }).join('');
507
+ checkGroups(input, asciiInteger, groupSizes(locale, detected));
508
+ }
509
+
510
+ return parse(input, detected as C, locale, options);
511
+ }
package/src/paypal.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * PayPal's amount view for the REST APIs (Orders v2, Payments v2):
3
+ * https://developer.paypal.com/api/rest/reference/currency-codes/, encoded in data/paypal.json.
4
+ * `value` is a decimal string in major units, with at most 15 digits before the point:
5
+ * - HUF, JPY and TWD don't support decimals: "1000", never "1000.00" (ISO 4217 gives HUF and TWD
6
+ * 2);
7
+ * - every other listed currency has 2 decimals: "10.99".
8
+ * BRL, CNY and MYR are for in-country accounts only; that isn't checked here.
9
+ */
10
+ import { PAYPAL_EXPONENTS } from './generated/paypal.js';
11
+ import { type CurrencyInput, type Money } from './money.js';
12
+ import { fromProcessorDecimal, toProcessorDecimal } from './processor.js';
13
+ import type { RoundingMode } from './rounding.js';
14
+
15
+ export { PAYPAL_EXPONENTS };
16
+
17
+ /** The `value` pattern from PayPal's Orders v2 schema. */
18
+ const VALUE_PATTERN = /^((-?[0-9]+)|(-?([0-9]+)?[.][0-9]+))$/;
19
+
20
+ /** PayPal allows at most 15 digits before the decimal point. */
21
+ const MAX_INTEGER_DIGITS = 15;
22
+
23
+ /** The `value` field's maxLength in the Orders v2 schema. */
24
+ const MAX_VALUE_LENGTH = 32;
25
+
26
+ /** The number of decimals PayPal takes for a currency (JPY, HUF, TWD 0; the others 2). */
27
+ export function paypalExponent(currency: string): number {
28
+ const exponent = PAYPAL_EXPONENTS[currency.toUpperCase()];
29
+ if (exponent === undefined) {
30
+ throw new RangeError(`${currency} is not a PayPal currency`);
31
+ }
32
+ return exponent;
33
+ }
34
+
35
+ /** Whether PayPal lists the currency (case-insensitive). */
36
+ export function isPayPalCurrency(currency: string): boolean {
37
+ return PAYPAL_EXPONENTS[currency.toUpperCase()] !== undefined;
38
+ }
39
+
40
+ /** Options for the PayPal amount: how to round when PayPal needs a coarser amount. */
41
+ export interface PayPalAmountOptions {
42
+ /**
43
+ * How to round when PayPal needs fewer decimals than the value has (default 'unnecessary':
44
+ * throw).
45
+ */
46
+ readonly rounding?: RoundingMode;
47
+ }
48
+
49
+ /**
50
+ * The exact `value` string: toPayPalValue(money('10.99', 'USD')) === '10.99', HUF 1000 -> '1000'.
51
+ * Throws a RangeError for negative amounts (refunds are sent as positive amounts).
52
+ */
53
+ export function toPayPalValue(money: Money, options?: PayPalAmountOptions): string {
54
+ const rounding = options?.rounding ?? 'unnecessary';
55
+
56
+ // "The amount must be a positive number"; zero stays allowed because the same money object is
57
+ // used for breakdown fields such as a zero tax_total. Refunds are positive amounts.
58
+ const value = toProcessorDecimal(
59
+ 'PayPal',
60
+ money,
61
+ paypalExponent(money.currency),
62
+ rounding,
63
+ 'nonNegative',
64
+ );
65
+
66
+ const integerDigits = value.replace('-', '').split('.')[0]!;
67
+ if (integerDigits.length > MAX_INTEGER_DIGITS) {
68
+ throw new RangeError(
69
+ `${money} is too large for PayPal (at most 15 digits before the decimal point)`,
70
+ );
71
+ }
72
+ return value;
73
+ }
74
+
75
+ /** A PayPal `money` object: `{ currency_code, value }`. */
76
+ export function toPayPal(
77
+ money: Money,
78
+ options?: PayPalAmountOptions,
79
+ ): { currency_code: string; value: string } {
80
+ return { currency_code: money.currency.toUpperCase(), value: toPayPalValue(money, options) };
81
+ }
82
+
83
+ /** Reads a PayPal `money` object exactly: fromPayPal({ currency_code: 'USD', value: '230.00' }). */
84
+ export function fromPayPal<C extends CurrencyInput>(amount: {
85
+ currency_code: C;
86
+ value: string;
87
+ }): Money<Uppercase<C>> {
88
+ const code = amount.currency_code.toUpperCase() as Uppercase<C>;
89
+
90
+ // Validates the currency before parsing, so an unlisted currency is the error reported.
91
+ paypalExponent(code);
92
+ return fromProcessorDecimal('PayPal', amount.value, code, VALUE_PATTERN, MAX_VALUE_LENGTH);
93
+ }