|
| 1 | +/** |
| 2 | + * Portuguese number-to-words converter |
| 3 | + * @module languages/pt |
| 4 | + */ |
| 5 | + |
| 6 | +const PT_ONES = Object.freeze(['', 'um', 'dois', 'três', 'quatro', 'cinco', 'seis', 'sete', 'oito', 'nove']); |
| 7 | +const PT_TENS = Object.freeze(['', '', 'vinte', 'trinta', 'quarenta', 'cinquenta', 'sessenta', 'setenta', 'oitenta', 'noventa']); |
| 8 | +const PT_TEENS = Object.freeze(['dez', 'onze', 'doze', 'treze', 'catorze', 'quinze', 'dezesseis', 'dezessete', 'dezoito', 'dezenove']); |
| 9 | +const PT_HUNDREDS = Object.freeze(['', 'cento', 'duzentos', 'trezentos', 'quatrocentos', 'quinhentos', 'seiscentos', 'setecentos', 'oitocentos', 'novecentos']); |
| 10 | +const PT_ILLIONS = Object.freeze(['', 'mil', 'milhão', 'bilhão', 'trilhão', 'quatrilhão', 'quintilhão', 'sextilhão', 'septilhão', 'octilhão', 'nonilhão', 'decilhão']); |
| 11 | +const PT_ILLIONS_PLURAL = Object.freeze(['', 'mil', 'milhões', 'bilhões', 'trilhões', 'quatrilhões', 'quintilhões', 'sextilhões', 'septilhões', 'octilhões', 'nonilhões', 'decilhões']); |
| 12 | + |
| 13 | +const MAX_VALUE = 10n ** 36n - 1n; |
| 14 | + |
| 15 | +const group = (n) => Math.ceil(n.toString().length / 3) - 1; |
| 16 | +const power = (g) => 10n ** BigInt(g * 3); |
| 17 | +const segment = (n, g) => n % power(g + 1); |
| 18 | +const hundment = (n, g) => Number(segment(n, g) / power(g)); |
| 19 | + |
| 20 | +const hundredPt = (n) => { |
| 21 | + if (n < 100 || n >= 1000) return ''; |
| 22 | + const h = Math.floor(n / 100); |
| 23 | + const remainder = n % 100; |
| 24 | + // "cem" when exactly 100, "cento" otherwise |
| 25 | + if (h === 1 && remainder === 0) return 'cem'; |
| 26 | + return PT_HUNDREDS[h]; |
| 27 | +}; |
| 28 | + |
| 29 | +const tenPt = (n) => { |
| 30 | + if (n === 0) return ''; |
| 31 | + if (n < 10) return PT_ONES[n]; |
| 32 | + if (n < 20) return PT_TEENS[n - 10]; |
| 33 | + const onesDigit = n % 10; |
| 34 | + if (onesDigit) return `${PT_TENS[Math.floor(n / 10)]} e ${PT_ONES[onesDigit]}`; |
| 35 | + return PT_TENS[Math.floor(n / 10)]; |
| 36 | +}; |
| 37 | + |
| 38 | +const cap = (str, style) => { |
| 39 | + switch (style) { |
| 40 | + case 'title': |
| 41 | + return str.replace(/\w([^-\s]*)/g, (txt) => |
| 42 | + txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase() |
| 43 | + ); |
| 44 | + case 'upper': return str.toUpperCase(); |
| 45 | + case 'lower': return str.toLowerCase(); |
| 46 | + default: return str; |
| 47 | + } |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * Convert a number to Portuguese words |
| 52 | + * @param {number|bigint} n - The number to convert |
| 53 | + * @param {Object} [opt] - Options object |
| 54 | + * @returns {string|false} The Portuguese word representation |
| 55 | + * |
| 56 | + * @example |
| 57 | + * portuguese(42) // 'quarenta e dois' |
| 58 | + * portuguese(1000) // 'mil' |
| 59 | + */ |
| 60 | +const portuguese = (n, opt) => { |
| 61 | + let num; |
| 62 | + |
| 63 | + if (typeof n === 'bigint') { |
| 64 | + if (n < 0n || n > MAX_VALUE) return false; |
| 65 | + num = n; |
| 66 | + } else if (typeof n === 'number') { |
| 67 | + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; |
| 68 | + if (n > Number.MAX_SAFE_INTEGER) return false; |
| 69 | + num = BigInt(n); |
| 70 | + } else { |
| 71 | + return false; |
| 72 | + } |
| 73 | + |
| 74 | + if (num === 0n) return 'zero'; |
| 75 | + if (num === 1n) return 'um'; |
| 76 | + |
| 77 | + const parts = []; |
| 78 | + |
| 79 | + for (let i = group(num); i >= 0; i--) { |
| 80 | + const h = hundment(num, i); |
| 81 | + if (h > 0) { |
| 82 | + let part = ''; |
| 83 | + |
| 84 | + const hundreds = Math.floor(h / 100); |
| 85 | + const tens = h % 100; |
| 86 | + |
| 87 | + if (hundreds > 0) { |
| 88 | + part += hundredPt(h); |
| 89 | + if (tens > 0) part += ' e '; |
| 90 | + } |
| 91 | + |
| 92 | + if (tens > 0) { |
| 93 | + part += tenPt(tens); |
| 94 | + } |
| 95 | + |
| 96 | + if (i > 0) { |
| 97 | + // Add scale word (mil, milhão, etc.) |
| 98 | + if (i === 1) { |
| 99 | + // "mil" doesn't change for plural and doesn't need "um" before it |
| 100 | + if (h === 1) { |
| 101 | + part = 'mil'; |
| 102 | + } else { |
| 103 | + part += ' mil'; |
| 104 | + } |
| 105 | + } else { |
| 106 | + // milhão, bilhão, etc. - use singular for 1, plural for others |
| 107 | + const scaleWord = h === 1 ? PT_ILLIONS[i] : PT_ILLIONS_PLURAL[i]; |
| 108 | + part += ` ${scaleWord}`; |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + parts.push(part); |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + // Join parts with appropriate connectors |
| 117 | + let result = ''; |
| 118 | + for (let i = 0; i < parts.length; i++) { |
| 119 | + if (i > 0) { |
| 120 | + const prevPart = parts[i - 1]; |
| 121 | + const currPart = parts[i]; |
| 122 | + |
| 123 | + // Use "e" (and) for connecting in Portuguese when: |
| 124 | + // 1. Current part has no scale word (final small numbers) |
| 125 | + // 2. Previous part has a higher scale (milhão+) and current part has "mil" |
| 126 | + const prevHasHigherScale = prevPart && (prevPart.includes('ilhão') || prevPart.includes('ilhões')); |
| 127 | + const currHasMil = currPart && currPart.includes('mil'); |
| 128 | + const currHasNoScale = currPart && !currHasMil && !currPart.includes('ilhão') && !currPart.includes('ilhões'); |
| 129 | + |
| 130 | + if (currHasNoScale || (prevHasHigherScale && currHasMil)) { |
| 131 | + result += ' e '; |
| 132 | + } else { |
| 133 | + result += ' '; |
| 134 | + } |
| 135 | + } |
| 136 | + result += parts[i]; |
| 137 | + } |
| 138 | + |
| 139 | + result = result.trim(); |
| 140 | + if (opt?.cap) result = cap(result, opt.cap); |
| 141 | + return result; |
| 142 | +}; |
| 143 | + |
| 144 | +export default portuguese; |
| 145 | +export { portuguese }; |
0 commit comments