UNPKG

tanisa

Version:

An utility to convert Malagasy 🇲🇬 numbers, including decimals, into their word representations.

156 lines (134 loc) • 5.03 kB
import { MalagasyNumerals } from './dictionary' import { TanisaOptions, LargeNumberUnit } from './interface' export class Tanisa { public toWords(number: number | string, options?: TanisaOptions): string { const ignoreDecimal = options?.ignoreDecimal ?? false const decimalPlaces = options?.decimalPlaces ?? -1 const numStr = String(number).trim() const [integerPartStr, decimalPartStr] = numStr.split('.') const integerPartNum = parseInt(integerPartStr || '0', 10) if ( isNaN(integerPartNum) || (decimalPartStr && decimalPartStr.length > 0 && isNaN(parseInt(decimalPartStr, 10))) ) { throw new TypeError(`Invalid number input: "${number}"`) } if (numStr.trim().startsWith('-') && integerPartNum !== 0) { throw new RangeError('Negative numbers are not supported.') } if ( integerPartNum >= MalagasyNumerals.MAX_SUPPORTED_INTEGER || integerPartStr == MalagasyNumerals.MAX_SUPPORTED_INTEGER.toString() ) { throw new RangeError( `Number ${integerPartNum} exceeds the maximum supported value (${MalagasyNumerals.MAX_SUPPORTED_INTEGER}).` ) } const integerWords = this.convertInteger(integerPartNum) let decimalWords = '' const processDecimals = decimalPartStr && decimalPartStr.length > 0 && !ignoreDecimal && decimalPlaces !== 0 if (processDecimals) { let effectiveDecimalPartStr = decimalPartStr if (decimalPlaces > 0 && decimalPartStr.length > decimalPlaces) { effectiveDecimalPartStr = decimalPartStr.substring(0, decimalPlaces) } if (parseInt(effectiveDecimalPartStr || '0', 10) > 0) { let tempDecimalWords = '' for (let i = 0; i < effectiveDecimalPartStr.length; i++) { const digit = effectiveDecimalPartStr[i] if (digit === '0') { tempDecimalWords += MalagasyNumerals.GLUE_DECIMAL_ZERO } else { const remainingDecimal = effectiveDecimalPartStr.substring(i) tempDecimalWords += this.convertInteger( parseInt(remainingDecimal, 10) ) break } } if (tempDecimalWords) { decimalWords = MalagasyNumerals.GLUE_FAINGO + tempDecimalWords } } } return integerWords + decimalWords } private convertInteger(num: number): string { if (num === 0) { return MalagasyNumerals.ZERO } for (const unit of MalagasyNumerals.LARGE_NUMBER_UNITS) { if (num >= unit.threshold) { return this.formatLargeNumber(num, unit) } } return this.convertBelowThousand(num) } private formatLargeNumber(num: number, unit: LargeNumberUnit): string { const multiple = Math.floor(num / unit.threshold) const remainder = num % unit.threshold let prefix = '' // Use prefix only if multiple is greater than 1 OR // if multiple is 1 AND the unit is larger than 'arivo' (1000) if (multiple > 1) { prefix = this.convertInteger(multiple) + ' ' } else if (multiple === 1 && unit.threshold > 1000) { prefix = MalagasyNumerals.DIGITS[1] + ' ' } // If multiple is 1 and unit is 'arivo', no prefix is needed ("arivo", not "iray arivo") const basePart = prefix + unit.name if (remainder > 0) { const remainderText = this.convertInteger(remainder) return remainderText + MalagasyNumerals.GLUE_SY + basePart } else { return basePart } } private convertBelowThousand(num: number): string { // Assumes 0 < num < 1000 if (num >= 100) { const hundredMultiple = Math.floor(num / 100) const remainder = num % 100 const hundredWord = MalagasyNumerals.HUNDREDS[hundredMultiple] if (remainder === 0) { return hundredWord } else { const remainderWords = this.convertBelowHundred(remainder) let glue = MalagasyNumerals.GLUE_AMBY as string // Special rule: Use "sy" if hundred base >= 200 AND remainder >= 10 if (hundredMultiple >= 2 && remainder >= 10) { glue = MalagasyNumerals.GLUE_SY } // Use "iraika" for remainder 1 when connecting const finalRemainderWords = remainder === 1 ? MalagasyNumerals.CUSTOM_ONE : remainderWords return finalRemainderWords + glue + hundredWord } } return this.convertBelowHundred(num) } private convertBelowHundred(num: number): string { if (num >= 10) { const tenMultiple = Math.floor(num / 10) const remainder = num % 10 const tenWord = MalagasyNumerals.TENS[tenMultiple] if (remainder === 0) { return tenWord } else { // Always use "amby" for tens+digits const digitWord = remainder === 1 ? MalagasyNumerals.CUSTOM_ONE : MalagasyNumerals.DIGITS[remainder] return digitWord + MalagasyNumerals.GLUE_AMBY + tenWord } } return MalagasyNumerals.DIGITS[num] } }