diff --git a/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems b/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems index 29ca4b6fb..9b1eb1b94 100644 --- a/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems +++ b/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems @@ -115,6 +115,7 @@ + diff --git a/src/Humanizer.Tests.Shared/Localisation/pt/NumberToWordsTests.cs b/src/Humanizer.Tests.Shared/Localisation/pt/NumberToWordsTests.cs new file mode 100644 index 000000000..8ff319ec9 --- /dev/null +++ b/src/Humanizer.Tests.Shared/Localisation/pt/NumberToWordsTests.cs @@ -0,0 +1,251 @@ +using Xunit; + +namespace Humanizer.Tests.Localisation.pt +{ + [UseCulture("pt")] + public class NumberToWordsTests + { + [Theory] + [InlineData(1, "um")] + [InlineData(2, "dois")] + [InlineData(3, "três")] + [InlineData(4, "quatro")] + [InlineData(5, "cinco")] + [InlineData(6, "seis")] + [InlineData(7, "sete")] + [InlineData(8, "oito")] + [InlineData(9, "nove")] + [InlineData(10, "dez")] + [InlineData(11, "onze")] + [InlineData(12, "doze")] + [InlineData(13, "treze")] + [InlineData(14, "quatorze")] + [InlineData(15, "quinze")] + [InlineData(16, "dezesseis")] + [InlineData(17, "dezessete")] + [InlineData(18, "dezoito")] + [InlineData(19, "dezenove")] + [InlineData(20, "vinte")] + [InlineData(30, "trinta")] + [InlineData(40, "quarenta")] + [InlineData(50, "cinquenta")] + [InlineData(51, "cinquenta e um")] + [InlineData(60, "sessenta")] + [InlineData(66, "sessenta e seis")] + [InlineData(70, "setenta")] + [InlineData(80, "oitenta")] + [InlineData(90, "noventa")] + [InlineData(100, "cem")] + [InlineData(200, "duzentos")] + [InlineData(300, "trezentos")] + [InlineData(400, "quatrocentos")] + [InlineData(500, "quinhentos")] + [InlineData(600, "seiscentos")] + [InlineData(700, "setecentos")] + [InlineData(800, "oitocentos")] + [InlineData(900, "novecentos")] + [InlineData(1000, "mil")] + [InlineData(2000, "dois mil")] + [InlineData(3000, "três mil")] + [InlineData(4000, "quatro mil")] + [InlineData(5000, "cinco mil")] + [InlineData(6000, "seis mil")] + [InlineData(7000, "sete mil")] + [InlineData(8000, "oito mil")] + [InlineData(9000, "nove mil")] + [InlineData(10000, "dez mil")] + [InlineData(100000, "cem mil")] + [InlineData(1000000, "um milhão")] + [InlineData(1000000000, "mil milhões")] + [InlineData(37, "trinta e sete")] + [InlineData(637, "seiscentos e trinta e sete")] + [InlineData(1637, "mil seiscentos e trinta e sete")] + [InlineData(61637, "sessenta e um mil seiscentos e trinta e sete")] + [InlineData(961637, "novecentos e sessenta e um mil seiscentos e trinta e sete")] + [InlineData(5961637, "cinco milhões novecentos e sessenta e um mil seiscentos e trinta e sete")] + [InlineData(25961637, "vinte e cinco milhões novecentos e sessenta e um mil seiscentos e trinta e sete")] + [InlineData(425961637, "quatrocentos e vinte e cinco milhões novecentos e sessenta e um mil seiscentos e trinta e sete")] + [InlineData(10000000, "dez milhões")] + [InlineData(100000000, "cem milhões")] + [InlineData(1101111101, "mil milhões cento e um milhões cento e onze mil cento e um")] + [InlineData(111, "cento e onze")] + [InlineData(1111, "mil cento e onze")] + [InlineData(1111101, "um milhão cento e onze mil cento e um")] + [InlineData(111111, "cento e onze mil cento e onze")] + [InlineData(1111111, "um milhão cento e onze mil cento e onze")] + [InlineData(11111111, "onze milhões cento e onze mil cento e onze")] + [InlineData(111111111, "cento e onze milhões cento e onze mil cento e onze")] + [InlineData(1111111111, "mil milhões cento e onze milhões cento e onze mil cento e onze")] + [InlineData(122, "cento e vinte e dois")] + [InlineData(123, "cento e vinte e três")] + [InlineData(1234, "mil duzentos e trinta e quatro")] + [InlineData(12345, "doze mil trezentos e quarenta e cinco")] + [InlineData(123456, "cento e vinte e três mil quatrocentos e cinquenta e seis")] + [InlineData(1234567, "um milhão duzentos e trinta e quatro mil quinhentos e sessenta e sete")] + [InlineData(12345678, "doze milhões trezentos e quarenta e cinco mil seiscentos e setenta e oito")] + [InlineData(123456789, "cento e vinte e três milhões quatrocentos e cinquenta e seis mil setecentos e oitenta e nove")] + [InlineData(1234567890, "mil milhões duzentos e trinta e quatro milhões quinhentos e sessenta e sete mil oitocentos e noventa")] + [InlineData(1999, "mil novecentos e noventa e nove")] + [InlineData(2000000, "dois milhões")] + [InlineData(2000000000, "dois mil milhões")] + [InlineData(2001000000, "dois mil milhões um milhão")] + [InlineData(2014, "dois mil e quatorze")] + [InlineData(2048, "dois mil e quarenta e oito")] + [InlineData(21, "vinte e um")] + [InlineData(211, "duzentos e onze")] + [InlineData(2111101, "dois milhões cento e onze mil cento e um")] + [InlineData(221, "duzentos e vinte e um")] + [InlineData(3501, "três mil quinhentos e um")] + [InlineData(8100, "oito mil e cem")] + public void ToWords(int number, string expected) + { + Assert.Equal(expected, number.ToWords()); + } + + [Theory] + [InlineData(1, "uma")] + [InlineData(2, "duas")] + [InlineData(3, "três")] + [InlineData(11, "onze")] + [InlineData(21, "vinte e uma")] + [InlineData(122, "cento e vinte e duas")] + [InlineData(232, "duzentas e trinta e duas")] + [InlineData(343, "trezentas e quarenta e três")] + [InlineData(3501, "três mil quinhentas e uma")] + [InlineData(100, "cem")] + [InlineData(1000, "mil")] + [InlineData(111, "cento e onze")] + [InlineData(1111, "mil cento e onze")] + [InlineData(111111, "cento e onze mil cento e onze")] + [InlineData(1111101, "um milhão cento e onze mil cento e uma")] + [InlineData(1111111, "um milhão cento e onze mil cento e onze")] + [InlineData(2111102, "dois milhões cento e onze mil cento e duas")] + [InlineData(3111101, "três milhões cento e onze mil cento e uma")] + [InlineData(1101111101, "mil milhões cento e um milhões cento e onze mil cento e uma")] + [InlineData(2101111101, "dois mil milhões cento e um milhões cento e onze mil cento e uma")] + [InlineData(1234, "mil duzentas e trinta e quatro")] + [InlineData(8100, "oito mil e cem")] + [InlineData(12345, "doze mil trezentas e quarenta e cinco")] + public void ToFeminineWords(int number, string expected) + { + Assert.Equal(expected, number.ToWords(GrammaticalGender.Feminine)); + } + + [Theory] + [InlineData(0, "zero")] + [InlineData(1, "primeiro")] + [InlineData(2, "segundo")] + [InlineData(3, "terceiro")] + [InlineData(4, "quarto")] + [InlineData(5, "quinto")] + [InlineData(6, "sexto")] + [InlineData(7, "sétimo")] + [InlineData(8, "oitavo")] + [InlineData(9, "nono")] + [InlineData(10, "décimo")] + [InlineData(11, "décimo primeiro")] + [InlineData(12, "décimo segundo")] + [InlineData(13, "décimo terceiro")] + [InlineData(14, "décimo quarto")] + [InlineData(15, "décimo quinto")] + [InlineData(16, "décimo sexto")] + [InlineData(17, "décimo sétimo")] + [InlineData(18, "décimo oitavo")] + [InlineData(19, "décimo nono")] + [InlineData(20, "vigésimo")] + [InlineData(21, "vigésimo primeiro")] + [InlineData(22, "vigésimo segundo")] + [InlineData(30, "trigésimo")] + [InlineData(40, "quadragésimo")] + [InlineData(50, "quinquagésimo")] + [InlineData(60, "sexagésimo")] + [InlineData(70, "septuagésimo")] + [InlineData(80, "octogésimo")] + [InlineData(90, "nonagésimo")] + [InlineData(95, "nonagésimo quinto")] + [InlineData(96, "nonagésimo sexto")] + [InlineData(100, "centésimo")] + [InlineData(120, "centésimo vigésimo")] + [InlineData(121, "centésimo vigésimo primeiro")] + [InlineData(200, "ducentésimo")] + [InlineData(300, "trecentésimo")] + [InlineData(400, "quadringentésimo")] + [InlineData(500, "quingentésimo")] + [InlineData(600, "sexcentésimo")] + [InlineData(700, "septingentésimo")] + [InlineData(800, "octingentésimo")] + [InlineData(900, "noningentésimo")] + [InlineData(1000, "milésimo")] + [InlineData(1001, "milésimo primeiro")] + [InlineData(1021, "milésimo vigésimo primeiro")] + [InlineData(2021, "segundo milésimo vigésimo primeiro")] + [InlineData(10000, "décimo milésimo")] + [InlineData(10121, "décimo milésimo centésimo vigésimo primeiro")] + [InlineData(100000, "centésimo milésimo")] + [InlineData(1000000, "milionésimo")] + [InlineData(1000000000, "milésimo milionésimo")] + public void ToOrdinalWords(int number, string words) + { + Assert.Equal(words, number.ToOrdinalWords()); + } + + [Theory] + [InlineData(0, "zero")] + [InlineData(1, "primeira")] + [InlineData(2, "segunda")] + [InlineData(3, "terceira")] + [InlineData(4, "quarta")] + [InlineData(5, "quinta")] + [InlineData(6, "sexta")] + [InlineData(7, "sétima")] + [InlineData(8, "oitava")] + [InlineData(9, "nona")] + [InlineData(10, "décima")] + [InlineData(11, "décima primeira")] + [InlineData(12, "décima segunda")] + [InlineData(13, "décima terceira")] + [InlineData(14, "décima quarta")] + [InlineData(15, "décima quinta")] + [InlineData(16, "décima sexta")] + [InlineData(17, "décima sétima")] + [InlineData(18, "décima oitava")] + [InlineData(19, "décima nona")] + [InlineData(20, "vigésima")] + [InlineData(21, "vigésima primeira")] + [InlineData(22, "vigésima segunda")] + [InlineData(30, "trigésima")] + [InlineData(40, "quadragésima")] + [InlineData(50, "quinquagésima")] + [InlineData(60, "sexagésima")] + [InlineData(70, "septuagésima")] + [InlineData(80, "octogésima")] + [InlineData(90, "nonagésima")] + [InlineData(95, "nonagésima quinta")] + [InlineData(96, "nonagésima sexta")] + [InlineData(100, "centésima")] + [InlineData(120, "centésima vigésima")] + [InlineData(121, "centésima vigésima primeira")] + [InlineData(200, "ducentésima")] + [InlineData(300, "trecentésima")] + [InlineData(400, "quadringentésima")] + [InlineData(500, "quingentésima")] + [InlineData(600, "sexcentésima")] + [InlineData(700, "septingentésima")] + [InlineData(800, "octingentésima")] + [InlineData(900, "noningentésima")] + [InlineData(1000, "milésima")] + [InlineData(1001, "milésima primeira")] + [InlineData(1021, "milésima vigésima primeira")] + [InlineData(2021, "segunda milésima vigésima primeira")] + [InlineData(10000, "décima milésima")] + [InlineData(10121, "décima milésima centésima vigésima primeira")] + [InlineData(100000, "centésima milésima")] + [InlineData(1000000, "milionésima")] + [InlineData(1000000000, "milésima milionésima")] + public void ToFeminineOrdinalWords(int number, string words) + { + Assert.Equal(words, number.ToOrdinalWords(GrammaticalGender.Feminine)); + } + } +} + diff --git a/src/Humanizer/Configuration/NumberToWordsConverterRegistry.cs b/src/Humanizer/Configuration/NumberToWordsConverterRegistry.cs index dbec634a1..c40e1e2b3 100644 --- a/src/Humanizer/Configuration/NumberToWordsConverterRegistry.cs +++ b/src/Humanizer/Configuration/NumberToWordsConverterRegistry.cs @@ -13,6 +13,7 @@ public NumberToWordsConverterRegistry() Register("fa", new FarsiNumberToWordsConverter()); Register("es", new SpanishNumberToWordsConverter()); Register("pl", (culture) => new PolishNumberToWordsConverter(culture)); + Register("pt", new PortugueseNumberToWordsConverter()); Register("pt-BR", new BrazilianPortugueseNumberToWordsConverter()); Register("ro", new RomanianNumberToWordsConverter()); Register("ru", new RussianNumberToWordsConverter()); diff --git a/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs new file mode 100644 index 000000000..8e63bb704 --- /dev/null +++ b/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; + +namespace Humanizer.Localisation.NumberToWords +{ + internal class PortugueseNumberToWordsConverter : GenderedNumberToWordsConverter + { + private static readonly string[] PortugueseUnitsMap = { "zero", "um", "dois", "três", "quatro", "cinco", "seis", "sete", "oito", "nove", "dez", "onze", "doze", "treze", "quatorze", "quinze", "dezesseis", "dezessete", "dezoito", "dezenove" }; + private static readonly string[] PortugueseTensMap = { "zero", "dez", "vinte", "trinta", "quarenta", "cinquenta", "sessenta", "setenta", "oitenta", "noventa" }; + private static readonly string[] PortugueseHundredsMap = { "zero", "cento", "duzentos", "trezentos", "quatrocentos", "quinhentos", "seiscentos", "setecentos", "oitocentos", "novecentos" }; + + private static readonly string[] PortugueseOrdinalUnitsMap = { "zero", "primeiro", "segundo", "terceiro", "quarto", "quinto", "sexto", "sétimo", "oitavo", "nono" }; + private static readonly string[] PortugueseOrdinalTensMap = { "zero", "décimo", "vigésimo", "trigésimo", "quadragésimo", "quinquagésimo", "sexagésimo", "septuagésimo", "octogésimo", "nonagésimo" }; + private static readonly string[] PortugueseOrdinalHundredsMap = { "zero", "centésimo", "ducentésimo", "trecentésimo", "quadringentésimo", "quingentésimo", "sexcentésimo", "septingentésimo", "octingentésimo", "noningentésimo" }; + + public override string Convert(long input, GrammaticalGender gender) + { + if (input > Int32.MaxValue || input < Int32.MinValue) + { + throw new NotImplementedException(); + } + var number = (int)input; + + if (number == 0) + { + return "zero"; + } + + if (number < 0) + { + return string.Format("menos {0}", Convert(Math.Abs(number), gender)); + } + + var parts = new List(); + + if ((number / 1000000000) > 0) + { + // gender is not applied for billions + parts.Add(number / 1000000000 == 1 + ? "mil milhões" + : string.Format("{0} mil milhões", Convert(number / 1000000000))); + + number %= 1000000000; + } + + if ((number / 1000000) > 0) + { + // gender is not applied for millions + parts.Add(number / 1000000 >= 2 + ? string.Format("{0} milhões", Convert(number / 1000000, GrammaticalGender.Masculine)) + : string.Format("{0} milhão", Convert(number / 1000000, GrammaticalGender.Masculine))); + + number %= 1000000; + } + + if ((number / 1000) > 0) + { + // gender is not applied for thousands + parts.Add(number / 1000 == 1 ? "mil" : string.Format("{0} mil", Convert(number / 1000, GrammaticalGender.Masculine))); + number %= 1000; + } + + if ((number / 100) > 0) + { + if (number == 100) + { + parts.Add(parts.Count > 0 ? "e cem" : "cem"); + } + else + { + // Gender is applied to hundreds starting from 200 + parts.Add(ApplyGender(PortugueseHundredsMap[(number / 100)], gender)); + } + + number %= 100; + } + + if (number > 0) + { + if (parts.Count != 0) + { + parts.Add("e"); + } + + if (number < 20) + { + parts.Add(ApplyGender(PortugueseUnitsMap[number], gender)); + } + else + { + var lastPart = PortugueseTensMap[number / 10]; + if ((number % 10) > 0) + { + lastPart += string.Format(" e {0}", ApplyGender(PortugueseUnitsMap[number % 10], gender)); + } + + parts.Add(lastPart); + } + } + + return string.Join(" ", parts.ToArray()); + } + + public override string ConvertToOrdinal(int number, GrammaticalGender gender) + { + // N/A in Portuguese ordinal + if (number == 0) + { + return "zero"; + } + + var parts = new List(); + + if ((number / 1000000000) > 0) + { + parts.Add(number / 1000000000 == 1 + ? string.Format("{0} {1}", ApplyOrdinalGender("milésimo", gender), ApplyOrdinalGender("milionésimo", gender)) + : string.Format("{0} {1} {2}", Convert(number / 1000000000), ApplyOrdinalGender("milésimo", gender), ApplyOrdinalGender("milionésimo", gender))); + + number %= 1000000000; + } + + if ((number / 1000000) > 0) + { + parts.Add(number / 1000000 == 1 + ? ApplyOrdinalGender("milionésimo", gender) + : string.Format("{0} " + ApplyOrdinalGender("milionésimo", gender), ConvertToOrdinal(number / 1000000000, gender))); + + number %= 1000000; + } + + if ((number / 1000) > 0) + { + parts.Add(number / 1000 == 1 + ? ApplyOrdinalGender("milésimo", gender) + : string.Format("{0} " + ApplyOrdinalGender("milésimo", gender), ConvertToOrdinal(number / 1000, gender))); + + number %= 1000; + } + + if ((number / 100) > 0) + { + parts.Add(ApplyOrdinalGender(PortugueseOrdinalHundredsMap[number / 100], gender)); + number %= 100; + } + + if ((number / 10) > 0) + { + parts.Add(ApplyOrdinalGender(PortugueseOrdinalTensMap[number / 10], gender)); + number %= 10; + } + + if (number > 0) + { + parts.Add(ApplyOrdinalGender(PortugueseOrdinalUnitsMap[number], gender)); + } + + return string.Join(" ", parts.ToArray()); + } + + private static string ApplyGender(string toWords, GrammaticalGender gender) + { + if (gender != GrammaticalGender.Feminine) + { + return toWords; + } + + if (toWords.EndsWith("os")) + { + return toWords.Substring(0, toWords.Length - 2) + "as"; + } + + if (toWords.EndsWith("um")) + { + return toWords.Substring(0, toWords.Length - 2) + "uma"; + } + + if (toWords.EndsWith("dois")) + { + return toWords.Substring(0, toWords.Length - 4) + "duas"; + } + + return toWords; + } + + private static string ApplyOrdinalGender(string toWords, GrammaticalGender gender) + { + if (gender == GrammaticalGender.Feminine) + { + return toWords.TrimEnd('o') + 'a'; + } + + return toWords; + } + } +}