UNPKG

harfizer

Version:

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

337 lines (311 loc) 9.88 kB
/** * @fileoverview * The RussianLanguagePlugin class implements the LanguagePlugin interface * and provides methods for converting numbers, dates, and times into their * Russian textual representation. It handles integer and decimal numbers, * negative values, Gregorian date strings, and time strings (HH:mm). * * Note: The Persian solar calendar is specific to Persian; for Russian, * the Gregorian calendar is used with Russian month names. */ import { ConversionOptions, InputNumber, LanguagePlugin } from "../core"; export class RussianLanguagePlugin implements LanguagePlugin { private static readonly DEFAULT_SEPARATOR: string = " "; private static readonly ZERO_WORD: string = "ноль"; private static readonly NEGATIVE_WORD: string = "минус"; private static readonly SCALE: string[] = [ "", "тысяча", "миллион", "миллиард", "триллион", "квадриллион", ]; private static readonly DIGITS: string[] = [ "", "один", "два", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять", ]; private static readonly TEENS: string[] = [ "десять", "одиннадцать", "двенадцать", "тринадцать", "четырнадцать", "пятнадцать", "шестнадцать", "семнадцать", "восемнадцать", "девятнадцать", ]; private static readonly TENS: string[] = [ "", "", "двадцать", "тридцать", "сорок", "пятьдесят", "шестьдесят", "семьдесят", "восемьдесят", "девяносто", ]; private static readonly HUNDREDS: string[] = [ "", "сто", "двести", "триста", "четыреста", "пятьсот", "шестьсот", "семьсот", "восемьсот", "девятьсот", ]; 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.convertBelowThousand(value); } private convertBelowThousand(n: number): string { let result = ""; const hundreds = Math.floor(n / 100); const remainder = n % 100; if (hundreds > 0) { result += RussianLanguagePlugin.HUNDREDS[hundreds]; } if (remainder > 0) { if (remainder < 10) { result += (result ? " " : "") + RussianLanguagePlugin.DIGITS[remainder]; } else if (remainder < 20) { result += (result ? " " : "") + RussianLanguagePlugin.TEENS[remainder - 10]; } else { const tens = Math.floor(remainder / 10); const unit = remainder % 10; result += (result ? " " : "") + RussianLanguagePlugin.TENS[tens]; if (unit > 0) { result += " " + RussianLanguagePlugin.DIGITS[unit]; } } } return result; } private static splitIntoTriples(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 - 3); groups.unshift(str.substring(start, end)); str = str.substring(0, start); } return groups; } // New helper: convertYear to process 4-digit years with correct feminine forms. private convertYear(year: number): string { if (year < 1000) { return this.convertNumber(year); } const thousands = Math.floor(year / 1000); const remainder = year % 1000; let thousandsPart = ""; if (thousands === 1) { thousandsPart = "одна тысяча"; } else if (thousands === 2) { thousandsPart = "две тысячи"; } else { // For numbers 3 and above, determine proper form. const form = [2, 3, 4].includes(thousands % 10) && ![12, 13, 14].includes(thousands % 100) ? "тысячи" : "тысяч"; thousandsPart = this.convertNumber(thousands) + " " + form; } let remainderPart = remainder > 0 ? " " + this.convertNumber(remainder) : ""; return thousandsPart + remainderPart; } public convertNumber( input: InputNumber, options?: ConversionOptions ): string { const effectiveOptions: ConversionOptions = { ...options }; const zeroWord = effectiveOptions.customZeroWord || RussianLanguagePlugin.ZERO_WORD; const negativeWord = effectiveOptions.customNegativeWord || RussianLanguagePlugin.NEGATIVE_WORD; const separator = effectiveOptions.customSeparator || RussianLanguagePlugin.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); } if (integerPart.length > 66) { throw new Error("Error: Out of range."); } // Break integer part into triples. const triples: string[] = RussianLanguagePlugin.splitIntoTriples(integerPart); const wordParts: string[] = []; for (let i = 0; i < triples.length; i++) { const converted = this.convertTripleToWords(triples[i]); if (converted !== "") { const scaleIndex = triples.length - i - 1; let scaleWord = ""; if (scaleIndex > 0) { scaleWord = RussianLanguagePlugin.SCALE[scaleIndex]; } wordParts.push(converted + (scaleWord ? " " + scaleWord : "")); } } let result = wordParts.join(separator); // Process fractional part using " запятая ". 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; } public convertDateToWords( dateStr: string, calendar: "jalali" | "gregorian" = "gregorian" ): string { 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."); } const months = [ "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря", ]; const monthName = months[monthNum - 1]; const dayWords = this.convertNumber(dayStr); // Instead of using convertNumber for year, use convertYear for correct forms. const yearWords = this.convertYear(parseInt(yearStr, 10)); return `${dayWords} ${monthName} ${yearWords} года`; } 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); const minuteWords = this.convertNumber(minute); const hourSuffix = this.getHourSuffix(hour); const minuteSuffix = this.getMinuteSuffix(minute); if (minute === 0) { return `${hourWords} ${hourSuffix}`; } else { return `${hourWords} ${hourSuffix} ${minuteWords} ${minuteSuffix}`; } } private getHourSuffix(hour: number): string { if (hour % 10 === 1 && hour % 100 !== 11) { return "час"; } else if ( [2, 3, 4].includes(hour % 10) && ![12, 13, 14].includes(hour % 100) ) { return "часа"; } else { return "часов"; } } private getMinuteSuffix(minute: number): string { if (minute % 10 === 1 && minute % 100 !== 11) { return "минута"; } else if ( [2, 3, 4].includes(minute % 10) && ![12, 13, 14].includes(minute % 100) ) { return "минуты"; } else { return "минут"; } } }