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;
+ }
+ }
+}