using System; using System.Linq; using System.Text; using Developpez.Dotnet.Language.Grammar; namespace Developpez.Dotnet { /// /// Classe statique de conversion d'un nombre en toutes lettres /// (en français uniquement). /// /// Sources pour les règles d'écriture : /// - http://fr.wikipedia.org/wiki/Nombres_en_fran%C3%A7ais /// - http://www.miakinen.net/vrac/nombres . public static class NumberConverter { #region Private readonly fields private static readonly string[] _UNITSANDTENS; private static readonly string[] _HUNDREDS; private static readonly string[] _THOUSANDPOWERS; private static readonly string _MINUS; private static readonly string _DECIMALSEPARATOR; private static readonly string _ZERO; #endregion #region Constructor static NumberConverter() { _MINUS = "moins"; _DECIMALSEPARATOR = "virgule"; _ZERO = "zéro"; // Les nombres jusqu'à cent ont beaucoup "d'exceptions" de nomenclature (23/99). // Il est plus simple de les stocker dans un tableau que de les générer dynamiquement : _UNITSANDTENS = new string[100] { string.Empty, "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf", "vingt", "vingt et un", "vingt-deux", "vingt-trois", "vingt-quatre", "vingt-cinq", "vingt-six", "vingt-sept", "vingt-huit", "vingt-neuf", "trente", "trente et un", "trente-deux", "trente-trois", "trente-quatre", "trente-cinq", "trente-six", "trente-sept", "trente-huit", "trente-neuf", "quarante", "quarante et un", "quarante-deux", "quarante-trois", "quarante-quatre", "quarante-cinq", "quarante-six", "quarante-sept", "quarante-huit", "quarante-neuf", "cinquante", "cinquante et un", "cinquante-deux", "cinquante-trois", "cinquante-quatre", "cinquante-cinq", "cinquante-six", "cinquante-sept", "cinquante-huit", "cinquante-neuf", "soixante", "soixante et un", "soixante-deux", "soixante-trois", "soixante-quatre", "soixante-cinq", "soixante-six", "soixante-sept", "soixante-huit", "soixante-neuf", "soixante-dix", "soixante et onze", "soixante-douze", "soixante-treize", "soixante-quatorze", "soixante-quinze", "soixante-seize", "soixante-dix-sept", "soixante-dix-huit", "soixante-dix-neuf", "quatre-vingt", "quatre-vingt-un", "quatre-vingt-deux", "quatre-vingt-trois", "quatre-vingt-quatre", "quatre-vingt-cinq", "quatre-vingt-six", "quatre-vingt-sept", "quatre-vingt-huit", "quatre-vingt-neuf", "quatre-vingt-dix", "quatre-vingt-onze", "quatre-vingt-douze", "quatre-vingt-treize", "quatre-vingt-quatorze", "quatre-vingt-quinze", "quatre-vingt-seize", "quatre-vingt-dix-sept", "quatre-vingt-dix-huit", "quatre-vingt-dix-neuf" }; _HUNDREDS = new string[10] { string.Empty, "cent", "deux cent", "trois cent", "quatre cent", "cinq cent", "six cent", "sept cent", "huit cent", "neuf cent" }; _THOUSANDPOWERS = new string[7] { "trillion", "mille", "billion", "milliard", "million", "mille", string.Empty }; } #endregion #region Private methods /// /// Accorder le mot "vingt". /// /// Un "s" si le nombre se termine par vingt et n'est pas suivi de "mille". /// Chaîne vide sinon. private static string MakeTwentyAgree(int value, int thousandPower, NumeralAdjective numeralAdjective) { // Vingt prend un "s" à la fin, si et seulement si : // - il fait partie d'un nombre cardinal. // - il n'est pas situé avant "mille". // - il se termine par quatre-vingts. if (numeralAdjective == NumeralAdjective.Cardinal && thousandPower != 1 && thousandPower != 5 && value == 80) return "s"; else return string.Empty; } /// /// Accorder le mot "cent". /// /// Un "s" si le nombre est un multiple de 100, strictement supérieur à 100 et n'est pas suivi de "mille". /// Chaîne vide sinon. private static string MakeHundredAgree(long hundreds, long tensAndUnits, int thousandPower, NumeralAdjective numeralAdjective) { // Cent prend un "s" à la fin, si et seulement si : // - il fait partie d'un nombre cardinal. // - il n'est pas situé avant "mille". // - il n'est pas suivi de dizaines ni de chiffres (remainder == 0). // - il strictement supérieur à 100 (hundreds > 1). if (numeralAdjective == NumeralAdjective.Cardinal && hundreds > 1 && thousandPower != 1 && thousandPower != 5 && tensAndUnits == 0) return "s"; else return string.Empty; } /// /// Accorder la puissance de mille donnée. /// /// Un "s" si le nombre strictement supérieur à 1 et n'est pas "mille". /// Chaîne vide sinon. private static string MakeThousandPowerAgree(long value, int thousandPower) { // La puissance prend un "s" à la fin, si et seulement si : // - il strictement supérieur à 1. // - il n'est pas "mille". // - ce n'est pas les centaines et unités. if (value > 1 && thousandPower != 1 && thousandPower != 5 && thousandPower != 6) return "s"; else return string.Empty; } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Le nombre est négatif. /// Genre du nombre entier. /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. private static string InnerSpell(ulong value, bool negative, Gender gender, NumeralAdjective numeralAdjective) { // Zéro : if (value == 0) return _ZERO; StringBuilder result = new StringBuilder(); // Valeurs de 1 à 99 // (court-circuite la méthode HighNumbersSpell() pour de meilleures performances) : if (value < 100) result.AppendFormat("{0}{1}", NumberConverter._UNITSANDTENS[value], NumberConverter.MakeTwentyAgree((int)value, (NumberConverter._THOUSANDPOWERS.Length - 1), numeralAdjective)); // Valeurs de 100 à 999 // (court-circuite la méthode HighNumbersSpell() pour de meilleures performances) : else if (value < 1000) result.Append(NumberConverter.HundredsAndUnitsSpell((int)value, (NumberConverter._THOUSANDPOWERS.Length - 1), numeralAdjective)); else result.Append(NumberConverter.HighNumbersSpell(value, numeralAdjective)); // Négatif : if (negative) result.Insert(0, NumberConverter._MINUS + " "); // Genre : if (gender == Gender.Feminine && result[result.Length - 2] == 'u' && result[result.Length - 1] == 'n') result.Append("e"); return result.ToString().TrimEnd(' '); } /// /// Convertit le nombre entier en toutes lettres (nombres supérieurs à 1000). /// /// Nombre entier (supérieur à 1000). /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. private static string HighNumbersSpell(ulong value, NumeralAdjective numeralAdjective) { StringBuilder result = new StringBuilder(); ulong remainder = 0; ulong quotient = value; int[] groups = new int[7] { 0, 0, 0, 0, 0, 0, 0 }; int groupIndex = groups.Length - 1; // Découper le nombre en groupes de trois chiffres de la façon suivante : // [0]trillions,[1]milliers de billions,[2]billions,[3]milliards, // [4]millions,[5]milliers,[6]centaines et unités. while (quotient >= 1) { remainder = quotient % 1000; quotient = quotient / 1000; //Math.DivRem(quotient, 1000, out remainder); groups[groupIndex] = (int)remainder; groupIndex--; } // Générer le nombre en toutes lettres : for (groupIndex = 0; groupIndex < groups.Length; groupIndex++) { if (groups[groupIndex] == 0) continue; if (groupIndex == 1) { // Nombre de milliers (> 1 car "un mille" n'existe pas) : if (groups[1] > 1) result.AppendFormat("{0} ", NumberConverter.HundredsAndUnitsSpell(groups[groupIndex], groupIndex, numeralAdjective)); // Puissance des milliers (de billions) : result.AppendFormat("{0} ", NumberConverter._THOUSANDPOWERS[groupIndex]); // Ajouter la "billions" dans cette boucle, si le groupe des billions est nul : if (groups[2] == 0) result.AppendFormat("{0}s ", NumberConverter._THOUSANDPOWERS[2]);// Dans ce cas, billions prend un "s" car il y en a plus de mille. } // Exception "un mille" donne "mille" : else if (groupIndex == 5 && groups[5] == 1) result.AppendFormat("{0} ", NumberConverter._THOUSANDPOWERS[groupIndex]); // Cas général : else result.AppendFormat("{0} {1}{2} ", NumberConverter.HundredsAndUnitsSpell(groups[groupIndex], groupIndex, numeralAdjective), NumberConverter._THOUSANDPOWERS[groupIndex], NumberConverter.MakeThousandPowerAgree(groups[groupIndex], groupIndex)); } return result.ToString().TrimEnd(' '); } /// /// Convertit un nombre entier en toutes lettres (nombres entre 0 et 999). /// /// Nombre entier (entre 0 et 999). /// La puissance de mille du nombre. /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. private static string HundredsAndUnitsSpell(int value, int thousandPower, NumeralAdjective numeralAdjective) { if (value == 0) return string.Empty; int remainder = 0; int quotient = Math.DivRem(value, 100, out remainder); if (quotient > 0) { if (remainder > 0) return string.Format("{0}{1} {2}{3}", NumberConverter._HUNDREDS[quotient], NumberConverter.MakeHundredAgree(quotient, remainder, thousandPower, numeralAdjective), NumberConverter._UNITSANDTENS[remainder], NumberConverter.MakeTwentyAgree(remainder, thousandPower, numeralAdjective)); else return string.Format("{0}{1}", NumberConverter._HUNDREDS[quotient], NumberConverter.MakeHundredAgree(quotient, remainder, thousandPower, numeralAdjective)); } else return string.Format("{0}{1}", NumberConverter._UNITSANDTENS[remainder], NumberConverter.MakeTwentyAgree(remainder, thousandPower, numeralAdjective)); } private static string SpellDecimal(long integralPart, int zerosAfterDecimal, long decimalPart, int sign) { if (decimalPart == 0) return Spell(sign * integralPart); string zeros = Enumerable.Repeat(_ZERO + " ", zerosAfterDecimal).Join(string.Empty); return string.Format( "{0}{1} {2} {3}{4}", sign < 0 ? _MINUS + " " : string.Empty, Spell(integralPart), _DECIMALSEPARATOR, zeros, Spell(decimalPart)); } private static string SpellAmount(long integralPart, long decimalPart, string currencyName, string centsName, int sign) { if (integralPart > 1) currencyName += "s"; if (decimalPart > 1) centsName += "s"; if (decimalPart == 0) return string.Format("{0} {1}", Spell(sign * integralPart), currencyName); if (integralPart == 0) return string.Format("{0} {1}", Spell(sign * decimalPart), centsName); return string.Format( "{0} {1} et {2} {3}", Spell(sign * integralPart), currencyName, Spell(decimalPart), centsName); } private static void SplitDouble(double value, int decimalPlaces, bool trimTrailingDecimalZeros, out long integralPart, out long decimalPart, out int zerosAfterDecimal) { double i = Math.Truncate(value); double d = Math.Round(Math.Abs((value - i)), decimalPlaces); int z = 0; for (int k = 0; k < decimalPlaces; k++) { d *= 10; if (d < 1.0) z++; } integralPart = (long)Math.Abs(i); decimalPart = (long)Math.Round(d); while (trimTrailingDecimalZeros && decimalPart != 0 && decimalPart % 10 == 0) decimalPart /= 10; zerosAfterDecimal = z; } private static void SplitDecimal(decimal value, int decimalPlaces, bool trimTrailingDecimalZeros, out long integralPart, out long decimalPart, out int zerosAfterDecimal) { decimal i = Math.Truncate(value); decimal d = Math.Round(Math.Abs((value - i)), decimalPlaces); int z = 0; for (int k = 0; k < decimalPlaces; k++) { d *= 10; if (d < 1.0m) z++; } integralPart = (long)Math.Abs(i); decimalPart = (long)d; while (trimTrailingDecimalZeros && decimalPart != 0 && decimalPart % 10 == 0) decimalPart /= 10; zerosAfterDecimal = z; } #endregion #region Public methods /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Le nombre en toutes lettres. public static string Spell(int value) { return NumberConverter.Spell(value, Gender.Masculine); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Le nombre en toutes lettres. public static string Spell(int value, Gender gender) { return NumberConverter.Spell(value, gender, NumeralAdjective.Cardinal); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. public static string Spell(int value, Gender gender, NumeralAdjective numeralAdjective) { if (value == int.MinValue) return NumberConverter.InnerSpell((ulong)Math.Abs((long)value), true, gender, numeralAdjective); return NumberConverter.InnerSpell((ulong)Math.Abs(value), (value < 0), gender, numeralAdjective); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Le nombre en toutes lettres. public static string Spell(uint value) { return NumberConverter.Spell(value, Gender.Masculine); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Le nombre en toutes lettres. public static string Spell(uint value, Gender gender) { return NumberConverter.Spell(value, gender, NumeralAdjective.Cardinal); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. public static string Spell(uint value, Gender gender, NumeralAdjective numeralAdjective) { return NumberConverter.InnerSpell((ulong)value, false, gender, numeralAdjective); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Le nombre en toutes lettres. public static string Spell(long value) { return NumberConverter.Spell(value, Gender.Masculine); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Le nombre en toutes lettres. public static string Spell(long value, Gender gender) { return NumberConverter.Spell(value, gender, NumeralAdjective.Cardinal); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. public static string Spell(long value, Gender gender, NumeralAdjective numeralAdjective) { if (value == long.MinValue) return NumberConverter.InnerSpell((ulong)(-value), true, gender, numeralAdjective); return NumberConverter.InnerSpell((ulong)Math.Abs(value), (value < 0), gender, numeralAdjective); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Le nombre en toutes lettres. public static string Spell(ulong value) { return NumberConverter.Spell(value, Gender.Masculine); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Le nombre en toutes lettres. public static string Spell(ulong value, Gender gender) { return NumberConverter.Spell(value, gender, NumeralAdjective.Cardinal); } /// /// Convertit un nombre entier en toutes lettres. /// /// Nombre entier. /// Genre du nombre entier. /// Nature de l'adjectif numéral. /// Le nombre en toutes lettres. public static string Spell(ulong value, Gender gender, NumeralAdjective numeralAdjective) { return NumberConverter.InnerSpell(value, false, gender, numeralAdjective); } /// /// Convertit un nombre en virgule flottante en toutes lettres, avec le nombre de chiffres spécifié après la virgule. /// /// Nombre en virgule flottante /// Nombre de chiffres après la virgule /// Le nombre en toutes lettres public static string Spell(double value, int decimalPlaces) { long i; long d; int z; SplitDouble(value, decimalPlaces, true, out i, out d, out z); return SpellDecimal(i, z, d, Math.Sign(value)); } /// /// Convertit un nombre décimal en toutes lettres, avec le nombre de chiffres spécifié après la virgule. /// /// Nombre décimal /// Nombre de chiffres après la virgule /// Le nombre en toutes lettres public static string Spell(decimal value, int decimalPlaces) { long i; long d; int z; SplitDecimal(value, decimalPlaces, true, out i, out d, out z); return SpellDecimal(i, z, d, Math.Sign(value)); } /// /// Convertit un montant en toutes lettres, avec la devise spécifiée. /// /// Montant en virgule flottante /// Nom de la devise /// Nom du centime de la devise /// Le montant en toutes lettres public static string SpellAmount(double value, string currencyName, string centsName) { long i; long d; int z; SplitDouble(value, 2, false, out i, out d, out z); return SpellAmount(i, d, currencyName, centsName, Math.Sign(value)); } /// /// Convertit un montant en toutes lettres, avec la devise spécifiée. /// /// Montant décimal /// Nom de la devise /// Nom du centime de la devise /// Le montant en toutes lettres public static string SpellAmount(decimal value, string currencyName, string centsName) { long i; long d; int z; SplitDecimal(value, 2, false, out i, out d, out z); return SpellAmount(i, d, currencyName, centsName, Math.Sign(value)); } #endregion } }