UNPKG

harfizer

Version:

> **Convert numbers, dates, and times into words — in 7+ languages, with style.**

336 lines (308 loc) 10.8 kB
/** * @fileoverview * The ChineseLanguagePlugin class implements the LanguagePlugin interface * and provides methods for converting numbers, dates, and times into their * Chinese textual representation. It handles integer and decimal numbers, * negative values, Gregorian date strings, and time strings (HH:mm). * * Note: Since the Persian solar calendar is specific to Persian, for Chinese, * the Gregorian calendar is used and dates are formatted as "YYYY年MM月DD日". */ import { ConversionOptions, InputNumber, LanguagePlugin } from "../core"; export class ChineseLanguagePlugin implements LanguagePlugin { /** * Default separator used to join tokens. */ private static readonly DEFAULT_SEPARATOR: string = " "; /** * Chinese word for zero. */ private static readonly ZERO_WORD: string = "零"; /** * Word for negative numbers. */ private static readonly NEGATIVE_WORD: string = "负"; /** * Large scale units in Chinese for grouping by 4 digits. * For example: 10^0, 10^4, 10^8, 10^12, ... */ private static readonly SCALE: string[] = [ "", // 10^0 "万", // 10^4 "亿", // 10^8 "兆", // 10^12 ]; /** * Converts a number less than 10,000 into its Chinese textual representation. * * @param {number} n - Number less than 10,000. * @returns {string} The Chinese numeral representation. * * Examples: * 7 => "七" * 15 => "十五" * 42 => "四十二" * 300 => "三百" * 456 => "四百五十六" */ private convertBelowTenThousand(n: number): string { const digits = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"]; let result = ""; if (n < 10) { return digits[n]; } if (n < 100) { const tens = Math.floor(n / 10); const unit = n % 10; if (n === 10) { return "十"; } // For numbers between 11 and 19, omit "一" before "十" if (tens === 1) { result = "十"; } else { result = digits[tens] + "十"; } if (unit !== 0) { result += digits[unit]; } return result; } if (n < 1000) { const hundreds = Math.floor(n / 100); const remainder = n % 100; result = digits[hundreds] + "百"; if (remainder === 0) { return result; } // If remainder is less than 10, insert "零" if (remainder < 10) { result += "零" + this.convertBelowTenThousand(remainder); } else { // If tens digit is zero, also insert "零" if (Math.floor(remainder / 10) === 0) { result += "零" + this.convertBelowTenThousand(remainder); } else { result += this.convertBelowTenThousand(remainder); } } return result; } if (n < 10000) { const thousands = Math.floor(n / 1000); const remainder = n % 1000; result = thousands === 1 ? "千" : digits[thousands] + "千"; if (remainder === 0) { return result; } // If remainder < 100, may need a "零" if (remainder < 100) { result += "零" + this.convertBelowTenThousand(remainder); } else { result += this.convertBelowTenThousand(remainder); } return result; } return ""; } /** * Converts a group of up to 4 digits (as InputNumber) into its Chinese textual representation. * This method is required by the LanguagePlugin interface. * * @param {InputNumber} num - The number (up to 4 digits) to convert. * @param {any} [lexicon] - Ignored in Chinese conversion. * @param {string} [_separator] - Ignored in Chinese conversion. * @returns {string} The textual representation. */ public convertTripleToWords( num: InputNumber, lexicon?: any, _separator?: string ): string { const value = typeof num === "bigint" ? Number(num) : parseInt(num.toString(), 10); if (value === 0) return ""; return this.convertBelowTenThousand(value); } /** * Splits a numeric string into groups of 4 digits (from right to left). * * Example: "12345678" => ["1234", "5678"] * * @param {string | number} num - The number to be split. * @returns {string[]} An array of 4-digit groups. */ private static splitIntoQuadruples(num: number | string): string[] { let str: string = typeof num === "number" ? num.toString() : num; const groups: string[] = []; while (str.length > 0) { const end = str.length; const start = Math.max(0, end - 4); groups.unshift(str.substring(start, end)); str = str.substring(0, start); } return groups; } /** * Converts a given number (integer or decimal, possibly negative) into its Chinese textual form. * Handles custom options and converts the fractional part digit-by-digit using "点". * * @param {InputNumber} input - The number to be converted. * @param {ConversionOptions} [options] - Supported options: * - customZeroWord: override the default word for zero. * - customNegativeWord: override the default negative word. * - customSeparator: override the default separator between tokens. * @returns {string} The Chinese textual representation of the number. * @throws {Error} If the input format is invalid or if the number exceeds allowed range. */ public convertNumber( input: InputNumber, options?: ConversionOptions ): string { const effectiveOptions: ConversionOptions = { ...options }; const zeroWord = effectiveOptions.customZeroWord || ChineseLanguagePlugin.ZERO_WORD; const negativeWord = effectiveOptions.customNegativeWord || ChineseLanguagePlugin.NEGATIVE_WORD; const separator = effectiveOptions.customSeparator || ChineseLanguagePlugin.DEFAULT_SEPARATOR; let rawInput: string = typeof input === "bigint" ? input.toString() : input.toString().trim(); let isNegative = false; if (rawInput.startsWith("-")) { isNegative = true; rawInput = rawInput.slice(1).replace(/[,\s-]/g, ""); } else { rawInput = rawInput.replace(/[,\s-]/g, ""); } if (!/^\d+(\.\d+)?$/.test(rawInput)) { throw new Error("Error: Invalid input format."); } if (rawInput === "0" || rawInput === "0.0") { return zeroWord; } // Separate integer and fractional parts. let integerPart = rawInput; let fractionalPart = ""; const pointIndex = rawInput.indexOf("."); if (pointIndex > -1) { integerPart = rawInput.substring(0, pointIndex); fractionalPart = rawInput.substring(pointIndex + 1); } // Safety check for integer length. if (integerPart.length > 66) { throw new Error("Error: Out of range."); } // Split integer part into groups of 4 digits. const groups = ChineseLanguagePlugin.splitIntoQuadruples(integerPart); const tokens: string[] = []; const scale = ChineseLanguagePlugin.SCALE; const numGroups = groups.length; for (let i = 0; i < numGroups; i++) { const groupNum = parseInt(groups[i], 10); if (groupNum === 0) continue; const text = this.convertBelowTenThousand(groupNum); const scaleIndex = numGroups - i - 1; let token = text; if (scaleIndex > 0) { token += scale[scaleIndex]; } tokens.push(token); } let result = tokens.join(separator); // Process fractional part: convert each digit using Chinese numerals. if (fractionalPart.length > 0) { const digitNames = [ "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", ]; const fracTokens = fractionalPart .split("") .map((d) => digitNames[parseInt(d, 10)]); result += separator + "点" + separator + fracTokens.join(separator); } if (isNegative) { result = negativeWord + separator + result; } return result; } /** * Converts a Gregorian date string (in "YYYY/MM/DD" or "YYYY-MM-DD" format) * into its Chinese textual representation. * The output format is "YYYY年MM月DD日", where each part is converted using convertNumber. * * @param {string} dateStr - The date string to be converted. * @param {"jalali" | "gregorian"} [calendar="gregorian"] - Only Gregorian is supported for Chinese. * @returns {string} The Chinese textual form of the date. * @throws {Error} If the format is invalid or if the month is out of range. */ public convertDateToWords( dateStr: string, calendar: "jalali" | "gregorian" = "gregorian" ): string { // For Chinese, we use the Gregorian calendar and format as "YYYY年MM月DD日". const parts = dateStr.split(/[-\/]/); if (parts.length !== 3) { throw new Error( "Invalid date format. Expected 'YYYY/MM/DD' or 'YYYY-MM-DD'." ); } const [yearStr, monthStr, dayStr] = parts; const monthNum = parseInt(monthStr, 10); if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) { throw new Error("Invalid month in date."); } // Convert year, month, and day using convertNumber. const yearWords = this.convertNumber(yearStr); const monthWords = this.convertNumber(monthStr); const dayWords = this.convertNumber(dayStr); return `${yearWords}${monthWords}${dayWords}日`; } /** * Converts a time string in "HH:mm" format to its Chinese textual representation. * The output format is "<hour>时<minute>分". If minute is zero, only "<hour>时" is returned. * * @param {string} timeStr - The time string in "HH:mm" format. * @returns {string} The Chinese textual representation of the time. * @throws {Error} If the format is invalid or if hours/minutes are out of range. */ public convertTimeToWords(timeStr: string): string { const parts = timeStr.split(":"); if (parts.length !== 2) { throw new Error("Invalid time format. Expected format 'HH:mm'."); } const [hourStr, minuteStr] = parts; const hour = parseInt(hourStr, 10); const minute = parseInt(minuteStr, 10); if (isNaN(hour) || isNaN(minute)) { throw new Error( "Invalid time format. Hours and minutes should be numbers." ); } if (hour < 0 || hour > 23) { throw new Error("Invalid hour value. Hour should be between 0 and 23."); } if (minute < 0 || minute > 59) { throw new Error( "Invalid minute value. Minute should be between 0 and 59." ); } const hourWords = this.convertNumber(hour); let result = `${hourWords}时`; if (minute !== 0) { const minuteWords = this.convertNumber(minute); result += `${minuteWords}分`; } return result; } }