n2words 4.0.0 → 5.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 (309) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +14 -12
  3. package/dist/am-ET.js +2 -2
  4. package/dist/am-ET.umd.js +2 -2
  5. package/dist/am-Latn-ET.js +2 -2
  6. package/dist/am-Latn-ET.umd.js +2 -2
  7. package/dist/ar-SA.js +2 -2
  8. package/dist/ar-SA.umd.js +2 -2
  9. package/dist/az-AZ.js +2 -2
  10. package/dist/az-AZ.umd.js +2 -2
  11. package/dist/bn-BD.js +2 -2
  12. package/dist/bn-BD.umd.js +2 -2
  13. package/dist/cs-CZ.js +2 -2
  14. package/dist/cs-CZ.umd.js +2 -2
  15. package/dist/da-DK.js +2 -2
  16. package/dist/da-DK.umd.js +2 -2
  17. package/dist/de-DE.js +2 -2
  18. package/dist/de-DE.umd.js +2 -2
  19. package/dist/el-GR.js +2 -2
  20. package/dist/el-GR.umd.js +2 -2
  21. package/dist/en-AU.js +2 -2
  22. package/dist/en-AU.umd.js +2 -2
  23. package/dist/en-BD.js +2 -2
  24. package/dist/en-BD.umd.js +2 -2
  25. package/dist/en-CA.js +2 -2
  26. package/dist/en-CA.umd.js +2 -2
  27. package/dist/en-GB.js +2 -2
  28. package/dist/en-GB.umd.js +2 -2
  29. package/dist/en-GH.js +2 -2
  30. package/dist/en-GH.umd.js +2 -2
  31. package/dist/en-IE.js +2 -2
  32. package/dist/en-IE.umd.js +2 -2
  33. package/dist/en-IN.js +2 -2
  34. package/dist/en-IN.umd.js +2 -2
  35. package/dist/en-KE.js +2 -2
  36. package/dist/en-KE.umd.js +2 -2
  37. package/dist/en-MY.js +2 -2
  38. package/dist/en-MY.umd.js +2 -2
  39. package/dist/en-NG.js +2 -2
  40. package/dist/en-NG.umd.js +2 -2
  41. package/dist/en-NZ.js +2 -2
  42. package/dist/en-NZ.umd.js +2 -2
  43. package/dist/en-PH.js +2 -2
  44. package/dist/en-PH.umd.js +2 -2
  45. package/dist/en-PK.js +2 -2
  46. package/dist/en-PK.umd.js +2 -2
  47. package/dist/en-SG.js +2 -2
  48. package/dist/en-SG.umd.js +2 -2
  49. package/dist/en-US.js +2 -2
  50. package/dist/en-US.umd.js +2 -2
  51. package/dist/en-ZA.js +2 -2
  52. package/dist/en-ZA.umd.js +2 -2
  53. package/dist/es-ES.js +2 -2
  54. package/dist/es-ES.umd.js +2 -2
  55. package/dist/es-MX.js +2 -2
  56. package/dist/es-MX.umd.js +2 -2
  57. package/dist/es-US.js +2 -2
  58. package/dist/es-US.umd.js +2 -2
  59. package/dist/fa-IR.js +2 -2
  60. package/dist/fa-IR.umd.js +2 -2
  61. package/dist/fi-FI.js +2 -2
  62. package/dist/fi-FI.umd.js +2 -2
  63. package/dist/fil-PH.js +2 -2
  64. package/dist/fil-PH.umd.js +2 -2
  65. package/dist/fr-BE.js +2 -2
  66. package/dist/fr-BE.umd.js +2 -2
  67. package/dist/fr-FR.js +2 -2
  68. package/dist/fr-FR.umd.js +2 -2
  69. package/dist/gu-IN.js +2 -2
  70. package/dist/gu-IN.umd.js +2 -2
  71. package/dist/ha-NG.js +2 -2
  72. package/dist/ha-NG.umd.js +2 -2
  73. package/dist/hbo-IL.js +2 -2
  74. package/dist/hbo-IL.umd.js +2 -2
  75. package/dist/he-IL.js +2 -2
  76. package/dist/he-IL.umd.js +2 -2
  77. package/dist/hi-IN.js +2 -2
  78. package/dist/hi-IN.umd.js +2 -2
  79. package/dist/hr-HR.js +2 -2
  80. package/dist/hr-HR.umd.js +2 -2
  81. package/dist/hu-HU.js +2 -2
  82. package/dist/hu-HU.umd.js +2 -2
  83. package/dist/id-ID.js +2 -2
  84. package/dist/id-ID.umd.js +2 -2
  85. package/dist/it-IT.js +2 -2
  86. package/dist/it-IT.umd.js +2 -2
  87. package/dist/ja-JP.js +2 -2
  88. package/dist/ja-JP.umd.js +2 -2
  89. package/dist/ka-GE.js +2 -2
  90. package/dist/ka-GE.umd.js +2 -2
  91. package/dist/kn-IN.js +2 -2
  92. package/dist/kn-IN.umd.js +2 -2
  93. package/dist/ko-KR.js +2 -2
  94. package/dist/ko-KR.umd.js +2 -2
  95. package/dist/lt-LT.js +2 -2
  96. package/dist/lt-LT.umd.js +2 -2
  97. package/dist/lv-LV.js +2 -2
  98. package/dist/lv-LV.umd.js +2 -2
  99. package/dist/mr-IN.js +2 -2
  100. package/dist/mr-IN.umd.js +2 -2
  101. package/dist/ms-MY.js +2 -2
  102. package/dist/ms-MY.umd.js +2 -2
  103. package/dist/nb-NO.js +2 -2
  104. package/dist/nb-NO.umd.js +2 -2
  105. package/dist/nl-NL.js +2 -2
  106. package/dist/nl-NL.umd.js +2 -2
  107. package/dist/pa-IN.js +2 -2
  108. package/dist/pa-IN.umd.js +2 -2
  109. package/dist/pl-PL.js +2 -2
  110. package/dist/pl-PL.umd.js +2 -2
  111. package/dist/pt-BR.js +2 -0
  112. package/dist/pt-BR.umd.js +2 -0
  113. package/dist/pt-PT.js +2 -2
  114. package/dist/pt-PT.umd.js +2 -2
  115. package/dist/ro-RO.js +2 -2
  116. package/dist/ro-RO.umd.js +2 -2
  117. package/dist/ru-RU.js +2 -2
  118. package/dist/ru-RU.umd.js +2 -2
  119. package/dist/sr-Cyrl-RS.js +2 -2
  120. package/dist/sr-Cyrl-RS.umd.js +2 -2
  121. package/dist/sr-Latn-RS.js +2 -2
  122. package/dist/sr-Latn-RS.umd.js +2 -2
  123. package/dist/sv-SE.js +2 -2
  124. package/dist/sv-SE.umd.js +2 -2
  125. package/dist/sw-KE.js +2 -2
  126. package/dist/sw-KE.umd.js +2 -2
  127. package/dist/ta-IN.js +2 -2
  128. package/dist/ta-IN.umd.js +2 -2
  129. package/dist/te-IN.js +2 -2
  130. package/dist/te-IN.umd.js +2 -2
  131. package/dist/th-TH.js +2 -2
  132. package/dist/th-TH.umd.js +2 -2
  133. package/dist/tr-TR.js +2 -2
  134. package/dist/tr-TR.umd.js +2 -2
  135. package/dist/uk-UA.js +2 -2
  136. package/dist/uk-UA.umd.js +2 -2
  137. package/dist/ur-PK.js +2 -2
  138. package/dist/ur-PK.umd.js +2 -2
  139. package/dist/vi-VN.js +2 -2
  140. package/dist/vi-VN.umd.js +2 -2
  141. package/dist/yo-NG.js +2 -2
  142. package/dist/yo-NG.umd.js +2 -2
  143. package/dist/zh-Hans-CN.js +2 -2
  144. package/dist/zh-Hans-CN.umd.js +2 -2
  145. package/dist/zh-Hant-TW.js +2 -2
  146. package/dist/zh-Hant-TW.umd.js +2 -2
  147. package/package.json +53 -36
  148. package/src/am-ET.d.ts +3 -5
  149. package/src/am-ET.js +41 -16
  150. package/src/am-Latn-ET.d.ts +3 -5
  151. package/src/am-Latn-ET.js +45 -16
  152. package/src/ar-SA.d.ts +44 -18
  153. package/src/ar-SA.js +93 -40
  154. package/src/az-AZ.d.ts +3 -5
  155. package/src/az-AZ.js +58 -20
  156. package/src/bn-BD.d.ts +3 -5
  157. package/src/bn-BD.js +32 -16
  158. package/src/cs-CZ.d.ts +3 -6
  159. package/src/cs-CZ.js +66 -42
  160. package/src/da-DK.d.ts +3 -6
  161. package/src/da-DK.js +53 -48
  162. package/src/de-DE.d.ts +17 -11
  163. package/src/de-DE.js +88 -57
  164. package/src/el-GR.d.ts +3 -6
  165. package/src/el-GR.js +45 -32
  166. package/src/en-AU.d.ts +17 -11
  167. package/src/en-AU.js +56 -41
  168. package/src/en-BD.d.ts +17 -11
  169. package/src/en-BD.js +60 -41
  170. package/src/en-CA.d.ts +36 -18
  171. package/src/en-CA.js +67 -46
  172. package/src/en-GB.d.ts +17 -11
  173. package/src/en-GB.js +56 -41
  174. package/src/en-GH.d.ts +32 -3
  175. package/src/en-GH.js +104 -26
  176. package/src/en-IE.d.ts +17 -11
  177. package/src/en-IE.js +56 -41
  178. package/src/en-IN.d.ts +17 -11
  179. package/src/en-IN.js +60 -41
  180. package/src/en-KE.d.ts +28 -3
  181. package/src/en-KE.js +93 -26
  182. package/src/en-MY.d.ts +26 -3
  183. package/src/en-MY.js +91 -26
  184. package/src/en-NG.d.ts +17 -11
  185. package/src/en-NG.js +56 -41
  186. package/src/en-NZ.d.ts +32 -3
  187. package/src/en-NZ.js +85 -31
  188. package/src/en-PH.d.ts +32 -3
  189. package/src/en-PH.js +97 -26
  190. package/src/en-PK.d.ts +17 -11
  191. package/src/en-PK.js +60 -41
  192. package/src/en-SG.d.ts +28 -3
  193. package/src/en-SG.js +93 -26
  194. package/src/en-US.d.ts +36 -18
  195. package/src/en-US.js +70 -47
  196. package/src/en-ZA.d.ts +17 -11
  197. package/src/en-ZA.js +56 -41
  198. package/src/es-ES.d.ts +53 -21
  199. package/src/es-ES.js +104 -56
  200. package/src/es-MX.d.ts +53 -21
  201. package/src/es-MX.js +104 -56
  202. package/src/es-US.d.ts +53 -21
  203. package/src/es-US.js +92 -51
  204. package/src/fa-IR.d.ts +3 -5
  205. package/src/fa-IR.js +28 -13
  206. package/src/fi-FI.d.ts +3 -6
  207. package/src/fi-FI.js +47 -29
  208. package/src/fil-PH.d.ts +3 -5
  209. package/src/fil-PH.js +61 -28
  210. package/src/fr-BE.d.ts +31 -15
  211. package/src/fr-BE.js +128 -57
  212. package/src/fr-FR.d.ts +31 -16
  213. package/src/fr-FR.js +97 -60
  214. package/src/gu-IN.d.ts +3 -5
  215. package/src/gu-IN.js +31 -16
  216. package/src/ha-NG.d.ts +3 -5
  217. package/src/ha-NG.js +55 -27
  218. package/src/hbo-IL.d.ts +26 -12
  219. package/src/hbo-IL.js +92 -51
  220. package/src/he-IL.d.ts +17 -10
  221. package/src/he-IL.js +92 -50
  222. package/src/hi-IN.d.ts +3 -5
  223. package/src/hi-IN.js +30 -17
  224. package/src/hr-HR.d.ts +21 -10
  225. package/src/hr-HR.js +89 -33
  226. package/src/hu-HU.d.ts +3 -5
  227. package/src/hu-HU.js +57 -23
  228. package/src/id-ID.d.ts +3 -5
  229. package/src/id-ID.js +56 -23
  230. package/src/it-IT.d.ts +17 -11
  231. package/src/it-IT.js +74 -43
  232. package/src/ja-JP.d.ts +3 -6
  233. package/src/ja-JP.js +39 -26
  234. package/src/ka-GE.d.ts +3 -6
  235. package/src/ka-GE.js +38 -26
  236. package/src/kn-IN.d.ts +3 -5
  237. package/src/kn-IN.js +31 -16
  238. package/src/ko-KR.d.ts +3 -6
  239. package/src/ko-KR.js +34 -26
  240. package/src/lt-LT.d.ts +21 -11
  241. package/src/lt-LT.js +64 -42
  242. package/src/lv-LV.d.ts +21 -11
  243. package/src/lv-LV.js +79 -51
  244. package/src/mr-IN.d.ts +3 -5
  245. package/src/mr-IN.js +31 -16
  246. package/src/ms-MY.d.ts +3 -5
  247. package/src/ms-MY.js +58 -24
  248. package/src/nb-NO.d.ts +3 -6
  249. package/src/nb-NO.js +54 -34
  250. package/src/nl-NL.d.ts +41 -20
  251. package/src/nl-NL.js +111 -69
  252. package/src/pa-IN.d.ts +3 -5
  253. package/src/pa-IN.js +32 -16
  254. package/src/pl-PL.d.ts +21 -11
  255. package/src/pl-PL.js +69 -45
  256. package/src/pt-BR.d.ts +42 -0
  257. package/src/pt-BR.js +574 -0
  258. package/src/pt-PT.d.ts +17 -11
  259. package/src/pt-PT.js +80 -48
  260. package/src/ro-RO.d.ts +21 -11
  261. package/src/ro-RO.js +77 -39
  262. package/src/ru-RU.d.ts +35 -15
  263. package/src/ru-RU.js +100 -38
  264. package/src/sr-Cyrl-RS.d.ts +35 -15
  265. package/src/sr-Cyrl-RS.js +100 -38
  266. package/src/sr-Latn-RS.d.ts +35 -15
  267. package/src/sr-Latn-RS.js +100 -38
  268. package/src/sv-SE.d.ts +3 -6
  269. package/src/sv-SE.js +53 -34
  270. package/src/sw-KE.d.ts +3 -5
  271. package/src/sw-KE.js +50 -20
  272. package/src/ta-IN.d.ts +3 -5
  273. package/src/ta-IN.js +29 -17
  274. package/src/te-IN.d.ts +3 -5
  275. package/src/te-IN.js +31 -16
  276. package/src/th-TH.d.ts +3 -5
  277. package/src/th-TH.js +42 -19
  278. package/src/tr-TR.d.ts +17 -11
  279. package/src/tr-TR.js +63 -37
  280. package/src/uk-UA.d.ts +21 -10
  281. package/src/uk-UA.js +89 -33
  282. package/src/ur-PK.d.ts +3 -5
  283. package/src/ur-PK.js +32 -16
  284. package/src/utils/check-max.d.ts +26 -0
  285. package/src/utils/check-max.js +33 -0
  286. package/src/utils/expand-scientific.d.ts +0 -4
  287. package/src/utils/expand-scientific.js +7 -9
  288. package/src/utils/is-plain-object.d.ts +3 -4
  289. package/src/utils/is-plain-object.js +3 -4
  290. package/src/utils/parse-cardinal.d.ts +1 -2
  291. package/src/utils/parse-cardinal.js +12 -9
  292. package/src/utils/parse-currency.d.ts +1 -2
  293. package/src/utils/parse-currency.js +9 -11
  294. package/src/utils/parse-ordinal.d.ts +0 -1
  295. package/src/utils/parse-ordinal.js +9 -10
  296. package/src/utils/resolve-options.d.ts +17 -0
  297. package/src/utils/resolve-options.js +56 -0
  298. package/src/utils/scale.d.ts +49 -0
  299. package/src/utils/scale.js +65 -0
  300. package/src/vi-VN.d.ts +3 -6
  301. package/src/vi-VN.js +41 -28
  302. package/src/yo-NG.d.ts +3 -5
  303. package/src/yo-NG.js +49 -33
  304. package/src/zh-Hans-CN.d.ts +45 -20
  305. package/src/zh-Hans-CN.js +84 -31
  306. package/src/zh-Hant-TW.d.ts +45 -20
  307. package/src/zh-Hant-TW.js +85 -34
  308. package/src/utils/validate-options.d.ts +0 -8
  309. package/src/utils/validate-options.js +0 -16
package/src/pt-BR.js ADDED
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Portuguese (Brazil) language converter
3
+ *
4
+ * CLDR: pt-BR | Brazilian Portuguese as used in Brazil
5
+ *
6
+ * Portuguese-specific rules:
7
+ * - "e" conjunction between units, tens and hundreds: vinte e um, cento e um
8
+ * - "cem" for exact 100, "cento" for 100+ remainder
9
+ * - Irregular hundreds: duzentos, trezentos, quatrocentos, etc.
10
+ * - Short scale: milhão (10^6), bilhão (10^9), trilhão (10^12)
11
+ * - Omit "um" before "mil"
12
+ */
13
+
14
+ import { parseCardinalValue } from './utils/parse-cardinal.js'
15
+ import { parseCurrencyValue } from './utils/parse-currency.js'
16
+ import { parseOrdinalValue } from './utils/parse-ordinal.js'
17
+ import { checkMax } from './utils/check-max.js'
18
+ import { western } from './utils/scale.js'
19
+ import { resolveOptions } from './utils/resolve-options.js'
20
+
21
+ // ============================================================================
22
+ // Vocabulary (module-level constants)
23
+ // ============================================================================
24
+
25
+ const ONES = ['', 'um', 'dois', 'três', 'quatro', 'cinco', 'seis', 'sete', 'oito', 'nove']
26
+ const TEENS = ['dez', 'onze', 'doze', 'treze', 'quatorze', 'quinze', 'dezesseis', 'dezessete', 'dezoito', 'dezenove']
27
+ const TENS = ['', '', 'vinte', 'trinta', 'quarenta', 'cinquenta', 'sessenta', 'setenta', 'oitenta', 'noventa']
28
+
29
+ // Irregular hundreds
30
+ const HUNDREDS = ['', 'cento', 'duzentos', 'trezentos', 'quatrocentos', 'quinhentos', 'seiscentos', 'setecentos', 'oitocentos', 'novecentos']
31
+
32
+ const THOUSAND = 'mil'
33
+ const ZERO = 'zero'
34
+ const NEGATIVE = 'menos'
35
+ const DECIMAL_SEP = 'vírgula'
36
+
37
+ // Ordinal vocabulary
38
+ const ORDINAL_ONES = ['', 'primeiro', 'segundo', 'terceiro', 'quarto', 'quinto', 'sexto', 'sétimo', 'oitavo', 'nono']
39
+ const ORDINAL_TEENS = ['décimo', 'décimo primeiro', 'décimo segundo', 'décimo terceiro', 'décimo quarto', 'décimo quinto', 'décimo sexto', 'décimo sétimo', 'décimo oitavo', 'décimo nono']
40
+ const ORDINAL_TENS = ['', '', 'vigésimo', 'trigésimo', 'quadragésimo', 'quinquagésimo', 'sexagésimo', 'septuagésimo', 'octogésimo', 'nonagésimo']
41
+ const ORDINAL_HUNDREDS = ['', 'centésimo', 'ducentésimo', 'tricentésimo', 'quadringentésimo', 'quingentésimo', 'sexcentésimo', 'septingentésimo', 'octingentésimo', 'nongentésimo']
42
+
43
+ // ============================================================================
44
+ // Currency vocabulary
45
+ // ============================================================================
46
+
47
+ // Dicionário focado no uso no Brasil (centavos para dólar e euro em vez de cêntimos)
48
+ /** @type {Record<string, {major: string[], minor: string[]}>} */
49
+ const CURRENCIES = {
50
+ BRL: { major: ['real', 'reais'], minor: ['centavo', 'centavos'] },
51
+ USD: { major: ['dólar', 'dólares'], minor: ['centavo', 'centavos'] },
52
+ EUR: { major: ['euro', 'euros'], minor: ['centavo', 'centavos'] }, // No Brasil é comum falar "centavos de euro"
53
+ GBP: { major: ['libra', 'libras'], minor: ['pêni', 'pence'] },
54
+ JPY: { major: ['iene', 'ienes'], minor: ['sen', 'sen'] }, // Iene não tem subdivisão usada no dia a dia
55
+ }
56
+
57
+ // Fallback para caso o usuário passe uma moeda não mapeada (ex: 'CAD')
58
+ const DEFAULT_CURRENCY_WORDS = { major: ['unidade', 'unidades'], minor: ['centavo', 'centavos'] }
59
+
60
+ // ============================================================================
61
+ // Segment Building
62
+ // ============================================================================
63
+
64
+ /**
65
+ * Builds segment word for 0-999 with Portuguese "e" rules.
66
+ * Returns the word and whether it's an exact hundred (for "cem" handling).
67
+ * @param {number} n - Number 0-999
68
+ * @returns {{word: string, isExactHundred: boolean, startsWithHundreds?: boolean}} The segment word and hundred-related flags
69
+ */
70
+ function buildSegment(n) {
71
+ if (n === 0) return { word: '', isExactHundred: false }
72
+
73
+ // Special case: exact 100 is "cem"
74
+ if (n === 100) return { word: 'cem', isExactHundred: true }
75
+
76
+ const ones = n % 10
77
+ const tens = Math.trunc(n / 10) % 10
78
+ const hundreds = Math.trunc(n / 100)
79
+
80
+ const parts = []
81
+
82
+ // Hundreds
83
+ if (hundreds > 0) {
84
+ parts.push(HUNDREDS[hundreds])
85
+ }
86
+
87
+ // Tens and ones
88
+ if (tens === 1) {
89
+ // Teens (10-19)
90
+ parts.push(TEENS[ones])
91
+ }
92
+ else if (tens >= 2) {
93
+ if (ones > 0) {
94
+ // Tens + ones with "e": "vinte e um"
95
+ parts.push(TENS[tens] + ' e ' + ONES[ones])
96
+ }
97
+ else {
98
+ parts.push(TENS[tens])
99
+ }
100
+ }
101
+ else if (ones > 0) {
102
+ parts.push(ONES[ones])
103
+ }
104
+
105
+ // Join hundreds with "e": "cento e um", "duzentos e trinta e um"
106
+ const word = parts.join(' e ')
107
+
108
+ return { word, isExactHundred: hundreds > 0 && tens === 0 && ones === 0, startsWithHundreds: n >= 100 }
109
+ }
110
+
111
+ // ============================================================================
112
+ // Scale Word Lookup (Short Scale for pt-BR)
113
+ // ============================================================================
114
+
115
+ // Precompute scale words for singular and plural forms
116
+ // Index 1 = thousands, 2 = millions, 3 = billions (10^9), etc.
117
+ const SCALE_WORDS_SINGULAR = [
118
+ '', // 0 unused
119
+ THOUSAND, // 1: mil
120
+ 'milhão', // 2: 10^6
121
+ 'bilhão', // 3: 10^9
122
+ 'trilhão', // 4: 10^12
123
+ 'quatrilhão', // 5: 10^15
124
+ 'quintilhão', // 6: 10^18
125
+ 'sextilhão', // 7: 10^21
126
+ 'setilhão', // 8: 10^24
127
+ ]
128
+
129
+ const SCALE_WORDS_PLURAL = [
130
+ '', // 0 unused
131
+ THOUSAND, // 1: mil (same)
132
+ 'milhões', // 2: 10^6
133
+ 'bilhões', // 3: 10^9
134
+ 'trilhões', // 4: 10^12
135
+ 'quatrilhões', // 5: 10^15
136
+ 'quintilhões', // 6: 10^18
137
+ 'sextilhões', // 7: 10^21
138
+ 'setilhões', // 8: 10^24
139
+ ]
140
+
141
+ // Scale ordinal words (short scale for pt-BR). Module-scope so its length can
142
+ // derive the ordinal ceiling and so it isn't rebuilt on every call.
143
+ const SCALE_ORDINAL = ['', 'milésimo', 'milionésimo', 'bilionésimo', 'trilionésimo', 'quatrilionésimo', 'quintilionésimo', 'sextilionésimo']
144
+
145
+ // Supported magnitude ceilings (checked at the public entry points). Cardinal
146
+ // scale words reach index SCALE_WORDS_SINGULAR.length-1 (setilhão, 10^24), so
147
+ // cardinals/currency must stay below 10^27. The ordinal of a number whose
148
+ // lowest non-zero group is a scale group uses SCALE_ORDINAL, which is shorter,
149
+ // so ordinals must stay below 10^(SCALE_ORDINAL.length * 3).
150
+ export const cardinalMax = western(SCALE_WORDS_SINGULAR.length - 1)
151
+ export const ordinalMax = western(SCALE_ORDINAL.length - 1)
152
+ export const currencyMax = western(SCALE_WORDS_SINGULAR.length - 1)
153
+
154
+ // ============================================================================
155
+ // Conversion Functions
156
+ // ============================================================================
157
+
158
+ /**
159
+ * Converts a non-negative integer to Portuguese words.
160
+ * @param {bigint} n - Non-negative integer to convert
161
+ * @returns {string} Portuguese words
162
+ */
163
+ function integerToWords(n) {
164
+ if (n === 0n) return ZERO
165
+
166
+ // Fast path: numbers < 1000
167
+ if (n < 1000n) {
168
+ return buildSegment(Number(n)).word
169
+ }
170
+
171
+ // Fast path: numbers < 1,000,000 (thousands)
172
+ if (n < 1_000_000n) {
173
+ const thousands = Number(n / 1000n)
174
+ const remainder = Number(n % 1000n)
175
+
176
+ let result
177
+ if (thousands === 1) {
178
+ // "mil" not "um mil"
179
+ result = THOUSAND
180
+ }
181
+ else {
182
+ result = buildSegment(thousands).word + ' ' + THOUSAND
183
+ }
184
+
185
+ if (remainder > 0) {
186
+ const remainderResult = buildSegment(remainder)
187
+ // REGRA DO "E": Menor que 100 OU Centena Exata (ex: 500)
188
+ if (!remainderResult.startsWithHundreds || remainderResult.isExactHundred) {
189
+ result += ' e ' + remainderResult.word
190
+ }
191
+ else {
192
+ result += ' ' + remainderResult.word
193
+ }
194
+ }
195
+
196
+ return result
197
+ }
198
+
199
+ // For numbers >= 1,000,000, use scale decomposition
200
+ return buildLargeNumberWords(n)
201
+ }
202
+
203
+ /**
204
+ * Builds words for numbers >= 1,000,000.
205
+ * Uses BigInt division for faster segment extraction.
206
+ * @param {bigint} n - Number >= 1,000,000
207
+ * @returns {string} Portuguese words
208
+ */
209
+ function buildLargeNumberWords(n) {
210
+ // Extract segments using BigInt division
211
+ const segments = []
212
+ let temp = n
213
+ while (temp > 0n) {
214
+ segments.push(Number(temp % 1000n))
215
+ temp = temp / 1000n
216
+ }
217
+
218
+ // Find the first non-zero segment index
219
+ let firstNonZeroIdx = 0
220
+ for (let i = 0; i < segments.length; i++) {
221
+ if (segments[i] !== 0) {
222
+ firstNonZeroIdx = i
223
+ break
224
+ }
225
+ }
226
+
227
+ let result = ''
228
+ let prevWasScale = false
229
+
230
+ for (let i = segments.length - 1; i >= 0; i--) {
231
+ const segment = segments[i]
232
+ if (segment === 0) continue
233
+
234
+ const segmentResult = buildSegment(segment)
235
+ const isLastSegment = (i === firstNonZeroIdx)
236
+
237
+ // REGRA DO "E": Se for o último segmento e for < 100 OU centena exata (ex: 500)
238
+ if (result && isLastSegment && prevWasScale && (!segmentResult.startsWithHundreds || segmentResult.isExactHundred)) {
239
+ result += ' e'
240
+ }
241
+
242
+ if (result) result += ' '
243
+
244
+ if (i === 0) {
245
+ // Units segment
246
+ result += segmentResult.word
247
+ prevWasScale = false
248
+ }
249
+ else if (i === 1) {
250
+ // Thousands
251
+ if (segment === 1) {
252
+ result += THOUSAND
253
+ }
254
+ else {
255
+ result += segmentResult.word + ' ' + THOUSAND
256
+ }
257
+ prevWasScale = true
258
+ }
259
+ else {
260
+ // Million and above
261
+ const scaleWord = segment === 1 ? SCALE_WORDS_SINGULAR[i] : SCALE_WORDS_PLURAL[i]
262
+ if (segment === 1) {
263
+ result += 'um ' + scaleWord
264
+ }
265
+ else {
266
+ result += segmentResult.word + ' ' + scaleWord
267
+ }
268
+ prevWasScale = true
269
+ }
270
+ }
271
+
272
+ return result
273
+ }
274
+
275
+ /**
276
+ * Converts decimal digits to Portuguese words.
277
+ * @param {string} decimalPart - Decimal digits (without the point)
278
+ * @returns {string} Portuguese words for decimal part
279
+ */
280
+ function decimalPartToWords(decimalPart) {
281
+ let result = ''
282
+
283
+ // Handle leading zeros
284
+ let i = 0
285
+ while (i < decimalPart.length && decimalPart[i] === '0') {
286
+ if (result) result += ' '
287
+ result += ZERO
288
+ i++
289
+ }
290
+
291
+ // Convert remainder as a single number
292
+ const remainder = decimalPart.slice(i)
293
+ if (remainder) {
294
+ if (result) result += ' '
295
+ result += integerToWords(BigInt(remainder))
296
+ }
297
+
298
+ return result
299
+ }
300
+
301
+ /**
302
+ * Converts a numeric value to Portuguese words.
303
+ * @param {number | string | bigint} value - The numeric value to convert
304
+ * @returns {string} The number in Portuguese words
305
+ */
306
+ function toCardinal(value) {
307
+ const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
308
+ // Both the integer part and the decimal's significant digits are spelled via
309
+ // the scale builder, so both must clear the ceiling.
310
+ checkMax(integerPart, cardinalMax, decimalPart)
311
+
312
+ let result = ''
313
+
314
+ if (isNegative) {
315
+ result = NEGATIVE + ' '
316
+ }
317
+
318
+ result += integerToWords(integerPart)
319
+
320
+ if (decimalPart) {
321
+ result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
322
+ }
323
+
324
+ return result
325
+ }
326
+
327
+ // ============================================================================
328
+ // Ordinal Functions
329
+ // ============================================================================
330
+
331
+ /**
332
+ * Builds ordinal words for 0-999.
333
+ * @param {number} n - Number 0-999
334
+ * @returns {string} Portuguese ordinal words
335
+ */
336
+ function buildOrdinalSegment(n) {
337
+ if (n === 0) return ''
338
+
339
+ const ones = n % 10
340
+ const tens = Math.trunc(n / 10) % 10
341
+ const hundreds = Math.trunc(n / 100)
342
+
343
+ const parts = []
344
+
345
+ // Hundreds ordinal
346
+ if (hundreds > 0) {
347
+ parts.push(ORDINAL_HUNDREDS[hundreds])
348
+ }
349
+
350
+ // Tens and ones
351
+ if (tens === 1) {
352
+ // 10-19: use teens array (décimo, décimo primeiro, etc.)
353
+ parts.push(ORDINAL_TEENS[ones])
354
+ }
355
+ else if (tens >= 2) {
356
+ parts.push(ORDINAL_TENS[tens])
357
+ if (ones > 0) {
358
+ parts.push(ORDINAL_ONES[ones])
359
+ }
360
+ }
361
+ else if (ones > 0) {
362
+ parts.push(ORDINAL_ONES[ones])
363
+ }
364
+
365
+ return parts.join(' ')
366
+ }
367
+
368
+ /**
369
+ * Builds ordinal words for large numbers.
370
+ * @param {bigint} n - Non-negative integer
371
+ * @returns {string} Portuguese ordinal words
372
+ */
373
+ function buildLargeOrdinal(n) {
374
+ // Extract segments
375
+ const segments = []
376
+ let temp = n
377
+ while (temp > 0n) {
378
+ segments.push(Number(temp % 1000n))
379
+ temp = temp / 1000n
380
+ }
381
+
382
+ // Find the lowest non-zero segment (index 0 = units, lowest scale)
383
+ let lowestNonZeroIdx = 0
384
+ for (let i = 0; i < segments.length; i++) {
385
+ if (segments[i] !== 0) {
386
+ lowestNonZeroIdx = i
387
+ break
388
+ }
389
+ }
390
+
391
+ let result = ''
392
+
393
+ for (let i = segments.length - 1; i >= 0; i--) {
394
+ const segment = segments[i]
395
+ if (segment === 0) continue
396
+
397
+ if (result) result += ' '
398
+
399
+ if (i === lowestNonZeroIdx) {
400
+ // Last non-zero segment gets ordinal form
401
+ if (i === 0) {
402
+ // Units: just ordinal
403
+ result += buildOrdinalSegment(segment)
404
+ }
405
+ else if (segment === 1 && i > 0) {
406
+ // Exact scale: "milésimo", "milionésimo", etc.
407
+ result += SCALE_ORDINAL[i]
408
+ }
409
+ else {
410
+ // Segment + scale ordinal
411
+ result += buildOrdinalSegment(segment) + ' ' + SCALE_ORDINAL[i]
412
+ }
413
+ }
414
+ else {
415
+ // Higher segments use cardinal form
416
+ if (i === 0) {
417
+ result += buildSegment(segment).word
418
+ }
419
+ else if (i === 1) {
420
+ if (segment === 1) {
421
+ result += THOUSAND
422
+ }
423
+ else {
424
+ result += buildSegment(segment).word + ' ' + THOUSAND
425
+ }
426
+ }
427
+ else {
428
+ const scaleWord = segment === 1 ? SCALE_WORDS_SINGULAR[i] : SCALE_WORDS_PLURAL[i]
429
+ if (segment === 1) {
430
+ result += 'um ' + scaleWord
431
+ }
432
+ else {
433
+ result += buildSegment(segment).word + ' ' + scaleWord
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ return result
440
+ }
441
+
442
+ /**
443
+ * Converts a number to Portuguese ordinal words.
444
+ * @param {number | string | bigint} value - The number to convert
445
+ * @returns {string} Portuguese ordinal words
446
+ */
447
+ function toOrdinal(value) {
448
+ const n = parseOrdinalValue(value)
449
+ checkMax(n, ordinalMax)
450
+
451
+ // Fast path: 1-9
452
+ if (n < 10n) {
453
+ return ORDINAL_ONES[Number(n)]
454
+ }
455
+
456
+ // Fast path: 10-19
457
+ if (n < 20n) {
458
+ return ORDINAL_TEENS[Number(n) - 10]
459
+ }
460
+
461
+ // Fast path: 20-99
462
+ if (n < 100n) {
463
+ const ones = Number(n % 10n)
464
+ const tens = Number(n / 10n)
465
+ if (ones === 0) {
466
+ return ORDINAL_TENS[tens]
467
+ }
468
+ return ORDINAL_TENS[tens] + ' ' + ORDINAL_ONES[ones]
469
+ }
470
+
471
+ // Fast path: 100-999
472
+ if (n < 1000n) {
473
+ return buildOrdinalSegment(Number(n))
474
+ }
475
+
476
+ // Large numbers
477
+ return buildLargeOrdinal(n)
478
+ }
479
+
480
+ // ============================================================================
481
+ // Currency Functions
482
+ // ============================================================================
483
+
484
+ /**
485
+ * @typedef {object} CurrencyOptions
486
+ * @property {boolean} [and] - Include "e" between major and minor units
487
+ * @property {string} [currency] - Currency code (e.g., 'BRL', 'USD'); empty means auto-detect for pt-BR
488
+ */
489
+
490
+ /** @type {Required<CurrencyOptions>} */
491
+ export const currencyDefaults = { and: true, currency: '' }
492
+
493
+ /**
494
+ * Converts a number to Brazilian Portuguese currency words.
495
+ * @param {number | string | bigint} value - The amount to convert
496
+ * @param {CurrencyOptions} [options] Currency formatting options
497
+ * @returns {string} Brazilian Portuguese currency words
498
+ * @example
499
+ * toCurrency(42.50) // 'quarenta e dois reais e cinquenta centavos'
500
+ * toCurrency(42.50, {currency: 'USD'}) // 'quarenta e dois dólares e cinquenta centavos'
501
+ */
502
+ function toCurrency(value, options) {
503
+ const { isNegative, dollars: majorUnits, cents: minorUnits } = parseCurrencyValue(value)
504
+ checkMax(majorUnits, currencyMax)
505
+ const { and, currency } = resolveOptions(options, currencyDefaults)
506
+
507
+ // 1. Descobre a moeda informada ou busca automaticamente a padrão do país (pt-BR = BRL)
508
+ let currencyCode = currency
509
+ if (!currencyCode) {
510
+ try {
511
+ // Intl Locale Info (getCurrencies) is a newer TC39 API present at
512
+ // runtime in modern engines but not yet in the TS ES2022 lib types;
513
+ // augment the type locally rather than widen the project's lib.
514
+ const localeInfo = /** @type {Intl.Locale & { getCurrencies(): string[] }} */ (new Intl.Locale('pt-BR'))
515
+ currencyCode = localeInfo.getCurrencies?.()[0]
516
+ }
517
+ catch {
518
+ // Ignora erro em ambientes antigos (fallback garantido abaixo)
519
+ }
520
+ currencyCode = currencyCode || 'BRL' // Padrão absoluto para o Brasil
521
+ }
522
+ currencyCode = currencyCode.toUpperCase()
523
+
524
+ // 2. Busca os nomes no dicionário ou usa o fallback genérico
525
+ const currencyWords = CURRENCIES[currencyCode] || {
526
+ major: [currencyCode, currencyCode],
527
+ minor: DEFAULT_CURRENCY_WORDS.minor,
528
+ }
529
+
530
+ let result = ''
531
+
532
+ if (isNegative) {
533
+ result = NEGATIVE + ' '
534
+ }
535
+
536
+ const hasMajor = majorUnits > 0n
537
+ const hasMinor = minorUnits > 0n
538
+
539
+ if (!hasMajor && !hasMinor) {
540
+ return ZERO + ' ' + currencyWords.major[1]
541
+ }
542
+
543
+ // Parte inteira (Reais, Dólares...)
544
+ if (hasMajor) {
545
+ const majorText = integerToWords(majorUnits)
546
+ const majorUnit = majorUnits === 1n ? currencyWords.major[0] : currencyWords.major[1]
547
+ result += majorText + ' ' + majorUnit
548
+ }
549
+
550
+ // Parte decimal (Centavos...)
551
+ if (hasMinor) {
552
+ if (hasMajor) {
553
+ result += and ? ' e ' : ' '
554
+ }
555
+ const minorText = integerToWords(minorUnits)
556
+ const minorUnit = minorUnits === 1n ? currencyWords.minor[0] : currencyWords.minor[1]
557
+
558
+ // Ignora adicionar unidade de centavos se a moeda não os tiver (ex: JPY onde minor é string vazia)
559
+ if (minorUnit === '') {
560
+ result += minorText
561
+ }
562
+ else {
563
+ result += minorText + ' ' + minorUnit
564
+ }
565
+ }
566
+
567
+ return result
568
+ }
569
+
570
+ // ============================================================================
571
+ // Public API
572
+ // ============================================================================
573
+
574
+ export { toCardinal, toOrdinal, toCurrency }
package/src/pt-PT.d.ts CHANGED
@@ -1,14 +1,27 @@
1
+ export const cardinalMax: bigint;
2
+ export const ordinalMax: bigint;
3
+ export const currencyMax: bigint;
4
+ /**
5
+ * @typedef {object} CurrencyOptions
6
+ * @property {boolean} [and] - Include "e" between euros and cents
7
+ */
8
+ /** @type {Required<CurrencyOptions>} */
9
+ export const currencyDefaults: Required<CurrencyOptions>;
10
+ export type CurrencyOptions = {
11
+ /**
12
+ * - Include "e" between euros and cents
13
+ */
14
+ and?: boolean | undefined;
15
+ };
1
16
  /**
2
17
  * Converts a numeric value to Portuguese words.
3
18
  *
4
19
  * This is the main public API. It accepts any valid numeric input
5
20
  * (number, string, or bigint) and handles parsing internally.
6
- *
7
21
  * @param {number | string | bigint} value - The numeric value to convert
8
22
  * @returns {string} The number in Portuguese words
9
23
  * @throws {TypeError} If value is not a valid numeric type
10
24
  * @throws {Error} If value is not a valid number format
11
- *
12
25
  * @example
13
26
  * toCardinal(21) // 'vinte e um'
14
27
  * toCardinal(100) // 'cem'
@@ -17,10 +30,8 @@
17
30
  export function toCardinal(value: number | string | bigint): string;
18
31
  /**
19
32
  * Converts a number to Portuguese ordinal words.
20
- *
21
33
  * @param {number | string | bigint} value - The number to convert
22
34
  * @returns {string} Portuguese ordinal words
23
- *
24
35
  * @example
25
36
  * toOrdinal(1) // 'primeiro'
26
37
  * toOrdinal(21) // 'vigésimo primeiro'
@@ -29,17 +40,12 @@ export function toCardinal(value: number | string | bigint): string;
29
40
  export function toOrdinal(value: number | string | bigint): string;
30
41
  /**
31
42
  * Converts a number to Portuguese currency words (Euro).
32
- *
33
43
  * @param {number | string | bigint} value - The amount to convert
34
- * @param {Object} [options]
35
- * @param {boolean} [options.and=true] - Include "e" between euros and cents
44
+ * @param {CurrencyOptions} [options] Currency formatting options
36
45
  * @returns {string} Portuguese currency words
37
- *
38
46
  * @example
39
47
  * toCurrency(42.50) // 'quarenta e dois euros e cinquenta cêntimos'
40
48
  * toCurrency(1) // 'um euro'
41
49
  * toCurrency(0.01) // 'um cêntimo'
42
50
  */
43
- export function toCurrency(value: number | string | bigint, options?: {
44
- and?: boolean | undefined;
45
- }): string;
51
+ export function toCurrency(value: number | string | bigint, options?: CurrencyOptions): string;