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