UNPKG

bahttext

Version:

Change number to Thai pronunciation string

263 lines (223 loc) 7.43 kB
const EMPTY = '' const ONE = 'หนึ่ง' const TWO = 'สอง' const THREE_TO_NINE = ['สาม', 'สี่', 'ห้า', 'หก', 'เจ็ด', 'แปด', 'เก้า'] const ED = 'เอ็ด' const YEE = 'ยี่' const LAN = 'ล้าน' // Index 0-5 correspond to units,สิบ,ร้อย,พัน,หมื่น,แสน const DIGIT = [EMPTY, 'สิบ', 'ร้อย', 'พัน', 'หมื่น', 'แสน'] // Helpers to build word tables quickly const ONES = [EMPTY, ED, TWO, ...THREE_TO_NINE] const TENS = [ EMPTY, ...[EMPTY, YEE, ...THREE_TO_NINE].map(t => t + DIGIT[1]) // "สิบ" family ] // 0-99 lookup (fast satang conversion) const SUB_HUNDRED = TENS.reduce( (acc, t) => acc.concat(ONES.map(o => t + o)), [] ) // Special case: 1 should read "หนึ่ง" not "เอ็ด" SUB_HUNDRED[1] = ONE // Single-digit lookup for generic conversion (no suffix) const SUB_TEN = [EMPTY, ONE, TWO, ...THREE_TO_NINE] // Pre-compiled constants for frequently used combinations const YEE_SIB = YEE + DIGIT[1] // "ยี่สิบ" const SIB = DIGIT[1] // "สิบ" // Default fallback text used throughout tests const DEFAULT_RESULT = 'ศูนย์บาทถ้วน' /** * Convert number → Thai Baht text (supports number or numeric string) * Mirrors behaviour of previous implementation but with faster core. * * @public * @param {number|string} input * @returns {string} */ function bahttext (input) { // Early invalidation – keep behaviour expected by test suite if (input === null || input === undefined || typeof input === 'boolean' || Array.isArray(input) || (typeof input !== 'number' && typeof input !== 'string')) { return DEFAULT_RESULT } const converted = convertInternal(input) return converted === false ? DEFAULT_RESULT : converted } /** * @private * @param {string} numStr – e.g. "123" (no sign, may include leading zeros) * @returns {string} */ function numberToWords (numStr) { // Short-circuit trivial "0" if (numStr === '0') return '' let output = EMPTY const len = numStr.length for (let i = 0; i < len; i++) { const d = numStr[i] const di = len - i - 1 // distance from rightmost digit const diMod = di % 6 // position inside a 6-digit group if (d === '0') { // skip zero – no wording } else if (diMod === 1) { // tens column inside group if (d === '1') { // "สิบ" output += SIB } else if (d === '2') { // "ยี่สิบ" output += YEE_SIB } else { const digitCode = d.charCodeAt(0) - 48 output += SUB_TEN[digitCode] + DIGIT[diMod] } } else if (diMod === 0 && d === '1' && i !== 0) { // trailing 1 (in ones place, not first digit in overall number) → "เอ็ด" // BUT: if it's a stand-alone digit in hundreds place (like 201), use "หนึ่ง" const isStandAloneInHundreds = (di === 0 && len > 1 && numStr[i - 1] === '0') output += isStandAloneInHundreds ? ONE : ED } else { const digitCode = d.charCodeAt(0) - 48 // faster than Number(d) output += SUB_TEN[digitCode] + DIGIT[diMod] } // add "ล้าน" after finishing each 6-digit block (except last group) if (diMod === 0 && di) { output += LAN } } return output } /** * @private * Handle numeric input and extract baht/satang values * @param {number} input - numeric input (positive or negative) * @returns {{baht: number, bahtStr: string, satang: number, isNegative: boolean} | false} */ function handleNumericInput (input) { if (!Number.isFinite(input)) return false if (input < Number.MIN_SAFE_INTEGER || input > Number.MAX_SAFE_INTEGER) { return false } const isNegative = input < 0 if (isNegative) { input = -input } const baht = Math.floor(input) const satang = Number.isInteger(input) ? 0 : Math.floor(((input + Number.EPSILON * (baht || 1)) * 100) % 100) const bahtStr = baht.toString() // slightly faster than String(baht) return { baht, bahtStr, satang, isNegative } } /** * @private * Handle string input and extract baht/satang values * @param {string} input - string input (numeric string, positive or negative) * @returns {{baht: number, bahtStr: string, satang: number, isNegative: boolean} | false} */ function handleStringInput (input) { let formatted = input.trim() if (formatted.startsWith('-')) { formatted = formatted.replace(/^-0+/, '-') // keep sign, drop leading zeros if (formatted === '-') formatted = '0' // catch "-" or "-0" } else { formatted = formatted.replace(/^0+/, '') || '0' // combine with empty check } // Not a valid number? if (formatted === '') formatted = '0' let inputNum = Number(formatted) if (Number.isNaN(inputNum)) return false const isNegative = inputNum < 0 if (isNegative) { inputNum = -inputNum // remove sign for string slicing later formatted = formatted.slice(1) } // Safe-range check after absolute value if (inputNum < Number.MIN_SAFE_INTEGER || inputNum > Number.MAX_SAFE_INTEGER) { return false } let baht, bahtStr, satang, satangStr // Handle decimal part by string slicing to avoid FP errors const periodIdx = formatted.lastIndexOf('.') if (periodIdx !== -1) { bahtStr = formatted.slice(0, periodIdx) || '0' baht = Number(bahtStr) satangStr = formatted.slice(periodIdx + 1) satang = satangStr ? Number(satangStr.slice(0, 2)) * (satangStr.length >= 2 ? 1 : [100, 10][satangStr.length]) : 0 } else { baht = inputNum bahtStr = formatted satang = 0 } return { baht, bahtStr, satang, isNegative } } /** * @private * Format satang portion of the output * @param {number} baht - baht amount * @param {number} satang - satang amount * @returns {string} formatted satang string */ function formatSatang (baht, satang) { if (satang) { let output = '' if (baht) output += 'บาท' output += SUB_HUNDRED[satang] + 'สตางค์' return output } else { return 'บาทถ้วน' } } /** * @private * Returns false when input cannot be parsed as numeric. */ function convertInternal (input) { let baht, bahtStr, satang let isNegative = false if (typeof input === 'number') { // Numeric input handling const result = handleNumericInput(input) if (result === false) return false baht = result.baht bahtStr = result.bahtStr satang = result.satang isNegative = result.isNegative } else if (typeof input === 'string') { // String input handling const result = handleStringInput(input) if (result === false) return false baht = result.baht bahtStr = result.bahtStr satang = result.satang isNegative = result.isNegative } else { // unreachable due to early guard, but for safety return false } // No value at all → zero if (!baht && !satang) { return DEFAULT_RESULT } // Build output let output = isNegative ? 'ลบ' : EMPTY // Baht part – using fast converter const bahtWords = numberToWords(bahtStr) output += bahtWords // Satang output += formatSatang(baht, satang) return output } module.exports = { bahttext, // Export private functions for testing handleNumericInput, handleStringInput, formatSatang } exports.default = { bahttext }