num2text
Version:
Convert numbers into words in multiple languages
260 lines (218 loc) • 7.99 kB
text/typescript
// src/index.ts
export type SupportedLang = 'en' | 'th' | 'zh' | 'zh-TW' | 'ja' | 'ko';
export function numberToWords(lang: SupportedLang, num: number): string {
switch (lang) {
case 'en': return numberToEnglish(num);
case 'th': return numberToThai(num);
case 'zh': return numberToChinese(num);
case 'zh-TW': return numberToTraditionalChinese(num);
case 'ja': return numberToJapanese(num);
case 'ko': return numberToKorean(num);
default: return num.toString();
}
}
// English (up to billion)
function numberToEnglish(num: number): string {
const units = [
'zero', 'one', 'two', 'three', 'four', 'five',
'six', 'seven', 'eight', 'nine', 'ten', 'eleven',
'twelve', 'thirteen', 'fourteen', 'fifteen',
'sixteen', 'seventeen', 'eighteen', 'nineteen'
];
const tens = [
'', '', 'twenty', 'thirty', 'forty', 'fifty',
'sixty', 'seventy', 'eighty', 'ninety'
];
const scales = ['', 'thousand', 'million', 'billion'];
if (num === 0) return units[0];
if (num < 0) return 'negative ' + numberToEnglish(-num);
function underThousand(n: number): string {
const hundred = Math.floor(n / 100);
const rest = n % 100;
let words = '';
if (hundred > 0) {
words += units[hundred] + ' hundred';
if (rest > 0) words += ' ';
}
if (rest > 0) {
if (rest < 20) {
words += units[rest];
} else {
const ten = Math.floor(rest / 10);
const unit = rest % 10;
words += tens[ten];
if (unit > 0) words += '-' + units[unit];
}
}
return words;
}
const parts: string[] = [];
const chunks: number[] = [];
let tempNum = num;
while (tempNum > 0) {
chunks.unshift(tempNum % 1000);
tempNum = Math.floor(tempNum / 1000);
}
for (let i = 0; i < chunks.length; i++) {
if (chunks[i] === 0) {
continue;
}
const scaleIndex = chunks.length - i - 1;
const chunkWords = underThousand(chunks[i]);
const scale = scaleIndex < scales.length ? scales[scaleIndex] : '';
parts.push(chunkWords + (scale ? ' ' + scale : ''));
}
return parts.join(' ').trim();
}
// Thai (Fixed)
function numberToThai(num: number): string {
if (num === 0) return 'ศูนย์';
if (num < 0) return 'ลบ' + numberToThai(-num);
const thDigits = ['ศูนย์','หนึ่ง','สอง','สาม','สี่','ห้า','หก','เจ็ด','แปด','เก้า'];
const thPositions = ['', 'สิบ', 'ร้อย', 'พัน', 'หมื่น', 'แสน'];
const MILLION = 1000000;
function readNumber(n: number): string {
if (n === 0) return '';
let result = '';
const sNum = n.toString();
const len = sNum.length;
let needZero = false;
for (let i = 0; i < len; i++) {
const digit = parseInt(sNum[i]);
const pos = len - i - 1;
if (digit === 0) {
// Check if we need to add "ศูนย์" for zero in the middle
// Only add zero if there's a gap between non-zero digits (not consecutive)
if (pos > 1 && result !== '') { // pos > 1 to avoid adding zero before tens/units
// Look ahead to see if there are non-zero digits
let hasNonZeroAfter = false;
for (let j = i + 1; j < len; j++) {
if (parseInt(sNum[j]) !== 0) {
hasNonZeroAfter = true;
break;
}
}
if (hasNonZeroAfter) {
needZero = true;
}
}
continue;
}
// Add zero if needed before this digit
if (needZero) {
//result += 'ศูนย์';
needZero = false;
}
if (pos === 0 && digit === 1 && len > 1) {
// หลักหน่วยเป็น 1 และไม่ใช่เลขหลักเดียว
result += 'เอ็ด';
} else if (pos === 1 && digit === 2) {
// หลักสิบเป็น 2
result += 'ยี่' + thPositions[pos];
} else if (pos === 1 && digit === 1) {
// หลักสิบเป็น 1
result += thPositions[pos];
} else {
result += thDigits[digit] + (pos < thPositions.length ? thPositions[pos] : '');
}
}
return result;
}
const parts: string[] = [];
let tempNum = num;
// จัดการหลักล้านขึ้นไป
while (tempNum >= MILLION) {
const millionPart = Math.floor(tempNum / MILLION);
parts.push(readNumber(millionPart) + 'ล้าน');
tempNum = tempNum % MILLION;
}
// จัดการหลักต่ำกว่าล้าน
if (tempNum > 0) {
parts.push(readNumber(tempNum));
}
return parts.join('');
}
// CJK Shared Base (Fixed)
function cjkConvert(num: number, digits: string[], units: string[], bigUnits: string[], suppressOne: boolean): string {
if (num === 0) return digits[0];
if (num < 0) return digits[0] + cjkConvert(-num, digits, units, bigUnits, suppressOne);
let result = '';
let unitIndex = 0;
let needZero = false;
while (num > 0) {
const fourDigitChunk = num % 10000;
let chunkString = '';
let hasNonZeroInCurrentChunk = false;
let innerNeedZero = false;
// Process digits within the 4-digit chunk (千, 百, 十, 一)
for (let i = 3; i >= 0; i--) {
const digit = Math.floor(fourDigitChunk / (10 ** i)) % 10;
if (digit === 0) {
if (i > 0 && hasNonZeroInCurrentChunk) {
innerNeedZero = true;
}
continue;
}
hasNonZeroInCurrentChunk = true;
// Add zero if needed within chunk
if (innerNeedZero) {
chunkString += digits[0];
innerNeedZero = false;
}
// Handle 'one' suppression for Japanese/Korean
const shouldSuppressOne = suppressOne && digit === 1 && i > 0;
if (!shouldSuppressOne) {
chunkString += digits[digit];
}
if (i > 0) {
chunkString += units[i];
}
}
if (hasNonZeroInCurrentChunk) {
// Add zero between chunks if needed
if (needZero && unitIndex > 0) {
result = digits[0] + result;
}
// Add big unit (万, 億) if not the first chunk
const scaleUnit = unitIndex < bigUnits.length ? bigUnits[unitIndex] : '';
result = chunkString + scaleUnit + result;
needZero = false;
} else if (unitIndex > 0 && result !== '') {
// This chunk is zero but there are non-zero chunks after it
needZero = true;
}
unitIndex++;
num = Math.floor(num / 10000);
}
// Clean up multiple zeros
result = result.replace(new RegExp(digits[0] + '{2,}', 'g'), digits[0]);
// Remove leading zero
if (result.startsWith(digits[0])) {
result = result.substring(1);
}
// Remove trailing zero
if (result.endsWith(digits[0])) {
result = result.substring(0, result.length - 1);
}
// Special case: if result is empty and suppressOne is true, return '一' or '일'
if (result === '' && suppressOne) {
return digits[1];
}
return result || digits[0];
}
// Chinese (Simplified)
function numberToChinese(num: number): string {
return cjkConvert(num, ['零','一','二','三','四','五','六','七','八','九'], ['', '十', '百', '千'], ['', '万', '亿'], false);
}
// Traditional Chinese (zh-TW)
function numberToTraditionalChinese(num: number): string {
return cjkConvert(num, ['零','壹','貳','參','肆','伍','陸','柒','捌','玖'], ['', '拾', '佰', '仟'], ['', '萬', '億'], false);
}
// Japanese
function numberToJapanese(num: number): string {
return cjkConvert(num, ['零','一','二','三','四','五','六','七','八','九'], ['', '十', '百', '千'], ['', '万', '億'], true);
}
// Korean
function numberToKorean(num: number): string {
return cjkConvert(num, ['영','일','이','삼','사','오','육','칠','팔','구'], ['', '십', '백', '천'], ['', '만', '억'], true);
}