harfizer
Version:
> **Convert numbers, dates, and times into words — in 7+ languages, with style.**
394 lines (393 loc) • 14.9 kB
JavaScript
;
/**
* @fileoverview
* The FrenchLanguagePlugin class implements the LanguagePlugin interface
* and provides methods for converting numbers, dates, and times into their
* French 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 French, the
* Gregorian calendar is used with French month names.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FrenchLanguagePlugin = void 0;
class FrenchLanguagePlugin {
/**
* **اضافه شده برای رفع خطای اینترفیس**
* Converts a three-digit number (or fewer) into its French textual representation.
* This method is required by the LanguagePlugin interface.
*
* @param {InputNumber} num - The three-digit number to convert.
* @param {any} [lexicon] - This parameter is ignored in French conversion.
* @param {string} [_separator] - This parameter is ignored in French conversion.
* @returns {string} The textual representation of the three-digit number.
*/
convertTripleToWords(num, lexicon, _separator) {
// تبدیل ورودی به عدد صحیح
const value = typeof num === "bigint" ? Number(num) : parseInt(num.toString(), 10);
if (value === 0)
return "";
return this.convertBelowThousand(value);
}
/**
* Converts a number below 100 into its French textual representation.
* Handles special cases such as 21, 31, etc. and the peculiarities of 70-79 and 80-99.
*
* @param {number} n - Number less than 100.
* @returns {string} The French words for the given number.
*/
convertBelowHundred(n) {
const units = [
"zéro",
"un",
"deux",
"trois",
"quatre",
"cinq",
"six",
"sept",
"huit",
"neuf",
];
const teens = [
"dix",
"onze",
"douze",
"treize",
"quatorze",
"quinze",
"seize",
];
if (n < 10) {
return units[n];
}
if (n >= 10 && n < 17) {
return teens[n - 10];
}
if (n < 20) {
// 17, 18, 19: dix-sept, dix-huit, dix-neuf
return "dix-" + units[n - 10];
}
// For numbers from 20 to 69.
if (n < 70) {
const tensArray = [
"",
"",
"vingt",
"trente",
"quarante",
"cinquante",
"soixante",
];
const tens = Math.floor(n / 10);
const unit = n % 10;
if (unit === 0) {
return tensArray[tens];
}
else if (unit === 1) {
// Exception: 21, 31, 41, 51, 61 -> "vingt et un", etc.
return tensArray[tens] + " et un";
}
else {
return tensArray[tens] + "-" + units[unit];
}
}
// For numbers from 70 to 79: expressed as 60 + 10..19.
if (n < 80) {
const remainder = n - 60;
if (remainder === 1) {
return "soixante et onze"; // 71
}
else {
return "soixante-" + this.convertBelowHundred(10 + remainder);
}
}
// For numbers from 80 to 99.
if (n < 100) {
if (n === 80) {
return "quatre-vingts";
}
const remainder = n - 80;
// Note: For 81, 91 etc., no "et" is used.
return "quatre-vingt-" + this.convertBelowHundred(remainder);
}
return "";
}
/**
* Converts a number below 1000 into its French textual representation.
* Handles the hundreds and the special plural rule for "cent".
*
* @param {number} n - Number less than 1000.
* @returns {string} The French words for the given number.
*/
convertBelowThousand(n) {
let words = "";
const hundreds = Math.floor(n / 100);
const remainder = n % 100;
if (hundreds > 0) {
if (hundreds === 1) {
words += "cent";
}
else {
words += this.convertBelowHundred(hundreds) + " cent";
}
// Add plural "s" to "cent" if no remainder and hundreds > 1.
if (remainder === 0 && hundreds > 1) {
words += "s";
}
}
if (remainder > 0) {
if (words !== "") {
words += " ";
}
words += this.convertBelowHundred(remainder);
}
return words;
}
/**
* Splits a numeric string into groups of three digits.
* Example: "1234567" => ["1", "234", "567"]
*
* @param {string | number} num - The number to be split.
* @returns {string[]} An array of three-digit groups.
*/
static splitIntoTriples(num) {
let str = typeof num === "number" ? num.toString() : num;
const triples = [];
while (str.length > 0) {
const end = str.length;
const start = Math.max(0, end - 3);
triples.unshift(str.substring(start, end));
str = str.substring(0, start);
}
return triples;
}
/**
* Converts a given number (integer or decimal, possibly negative) into its French textual form.
* Handles custom options and converts the fractional part by reading each digit.
*
* @param {InputNumber} input - The number to be converted.
* @param {ConversionOptions} [options] - Optional configuration.
* Supported options:
* - customZeroWord: override the default word for zero.
* - customNegativeWord: override the default negative word.
* - customSeparator: override the default separator between number groups.
* @returns {string} The French word representation of the given number.
* @throws {Error} If the input format is invalid or if the number is out of allowed range.
*/
convertNumber(input, options) {
const effectiveOptions = { ...options };
// استفاده از تنظیمات سفارشی در صورت وجود.
const zeroWord = effectiveOptions.customZeroWord || FrenchLanguagePlugin.ZERO_WORD;
const negativeWord = effectiveOptions.customNegativeWord || FrenchLanguagePlugin.NEGATIVE_WORD;
const separator = effectiveOptions.customSeparator ||
FrenchLanguagePlugin.DEFAULT_SEPARATOR;
let rawInput = typeof input === "bigint" ? input.toString() : input.toString().trim();
// Handle negative numbers and remove commas/spaces/dashes.
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.");
}
// Check if the number is zero.
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);
}
// Limit integer part length.
if (integerPart.length > 66) {
throw new Error("Error: Out of range.");
}
// Split the integer part into triples.
const triples = FrenchLanguagePlugin.splitIntoTriples(integerPart);
const tokens = [];
const scale = FrenchLanguagePlugin.SCALE;
const numTriples = triples.length;
// For each triple group, به صورت جداگانه عدد و واحد مقیاس را به آرایه توکنها اضافه میکنیم.
for (let i = 0; i < numTriples; i++) {
const groupNum = parseInt(triples[i], 10);
if (groupNum === 0)
continue;
const scaleIndex = numTriples - i - 1;
const scaleWord = scale[scaleIndex];
if (scaleIndex === 1 && groupNum === 1) {
// برای 1000، اگر عدد 1 باشد، فقط "mille" را اضافه میکنیم.
tokens.push(scaleWord);
}
else {
// در غیر این صورت، ابتدا عبارت عددی را اضافه میکنیم.
tokens.push(this.convertBelowThousand(groupNum));
if (scaleIndex > 0) {
// برای واحدهای بالاتر از 1، اگر مقیاس نیاز به جمع داشته باشد، اضافه میکنیم.
let finalScale = scaleIndex >= 2
? groupNum > 1
? scaleWord + "s"
: scaleWord
: scaleWord;
tokens.push(finalScale);
}
}
}
let result = tokens.join(separator);
// Handle fractional part: decimals are read digit by digit after "virgule".
if (fractionalPart.length > 0) {
const digitNames = [
"zéro",
"un",
"deux",
"trois",
"quatre",
"cinq",
"six",
"sept",
"huit",
"neuf",
];
const fracWords = fractionalPart
.split("")
.map((d) => digitNames[parseInt(d, 10)])
.join(separator);
result += separator + "virgule" + separator + fracWords;
}
if (isNegative) {
result = negativeWord + separator + result;
}
return result;
}
/**
* Converts a Gregorian date string (in "YYYY/MM/DD" or "YYYY-MM-DD" format)
* into its French textual representation.
* For day 1, "premier" is used; month names are in French.
*
* @param {string} dateStr - The date string to be converted.
* @param {"jalali" | "gregorian"} [calendar="gregorian"] - Only Gregorian is supported for French.
* @returns {string} The French textual form of the given date.
* @throws {Error} If the format is invalid or month is out of range.
*/
convertDateToWords(dateStr, calendar = "gregorian") {
// For French, only Gregorian calendar is used.
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 frenchMonths = [
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"août",
"septembre",
"octobre",
"novembre",
"décembre",
];
const monthName = frenchMonths[monthNum - 1];
// For day: use "premier" for 1, otherwise convert as number.
const dayNum = parseInt(dayStr, 10);
const dayWords = dayNum === 1 ? "premier" : this.convertNumber(dayNum);
const yearWords = this.convertNumber(yearStr);
return `${dayWords} ${monthName} ${yearWords}`;
}
/**
* Converts a time string in "HH:mm" format to its French textual representation.
* Example: "09:05" => "Il est neuf heures cinq minutes".
*
* @param {string} timeStr - The time string in "HH:mm" format.
* @returns {string} The French textual representation of the time.
* @throws {Error} If the format is invalid or hours/minutes are out of range.
*/
convertTimeToWords(timeStr) {
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.");
}
// Build the hour string.
// In French, 1 o'clock is "une heure" (feminine).
const hourWords = hour === 1 ? "une" : this.convertNumber(hour);
let timeText = "Il est " + hourWords + " heure";
if (hour !== 1) {
timeText += "s";
}
// Append minutes if not zero.
if (minute > 0) {
const minuteWords = minute === 1 ? "une" : this.convertNumber(minute);
timeText += " " + minuteWords + " minute" + (minute === 1 ? "" : "s");
}
return timeText;
}
}
exports.FrenchLanguagePlugin = FrenchLanguagePlugin;
/**
* Default separator for joining parts (space in French).
*/
FrenchLanguagePlugin.DEFAULT_SEPARATOR = " ";
/**
* Word for zero in French.
*/
FrenchLanguagePlugin.ZERO_WORD = "zéro";
/**
* Word used for negative numbers.
*/
FrenchLanguagePlugin.NEGATIVE_WORD = "moins";
/**
* Large scale units in French for thousand grouping.
* Extended to support up to 22 levels (e.g. décilliard).
* Note: "mille" is used without "un" for 1000.
*/
FrenchLanguagePlugin.SCALE = [
"", // 10^0
"mille", // 10^3
"million", // 10^6
"milliard", // 10^9
"billion", // 10^12
"billiard", // 10^15
"trillion", // 10^18
"trilliard", // 10^21
"quadrillion", // 10^24
"quadrilliard", // 10^27
"quintillion", // 10^30
"quintilliard", // 10^33
"sextillion", // 10^36
"sextilliard", // 10^39
"septillion", // 10^42
"septilliard", // 10^45
"octillion", // 10^48
"octilliard", // 10^51
"nonillion", // 10^54
"nonilliard", // 10^57
"décillion", // 10^60
"décilliard", // 10^63
];