diff --git a/.gitignore b/.gitignore index 4212daa65..6d38a7388 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,8 @@ obj/ *.swp *.orig *.nupkg -*.crunchproject.local.xml -*.crunchsolution.local.xml -*.ncrunchsolution -*.ncrunchproject +*ncrunch* +*crunch*.local.xml *.cache src/packages/* PackageBuild/* diff --git a/release_notes.md b/release_notes.md index 1ef033e67..9ba539be0 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,5 +1,6 @@ ###In Development - [#166](https://github.com/MehdiK/Humanizer/pull/166): Added Dutch (NL) Number to words and ordinals + - [#199](https://github.com/MehdiK/Humanizer/pull/199): Added Hebrew Number to words (both genders) [Commits](https://github.com/MehdiK/Humanizer/compare/v1.21.15...master) diff --git a/src/Humanizer.Tests/Humanizer.Tests.csproj b/src/Humanizer.Tests/Humanizer.Tests.csproj index 1291c6102..eb7af68d1 100644 --- a/src/Humanizer.Tests/Humanizer.Tests.csproj +++ b/src/Humanizer.Tests/Humanizer.Tests.csproj @@ -87,6 +87,7 @@ + diff --git a/src/Humanizer.Tests/Localisation/he/NumberToWordsTests.cs b/src/Humanizer.Tests/Localisation/he/NumberToWordsTests.cs new file mode 100644 index 000000000..ba54033fb --- /dev/null +++ b/src/Humanizer.Tests/Localisation/he/NumberToWordsTests.cs @@ -0,0 +1,115 @@ +using Xunit; +using Xunit.Extensions; + +namespace Humanizer.Tests.Localisation.he +{ + public class NumberToWordsTests : AmbientCulture + { + public NumberToWordsTests() : base("he") { } + + [Theory] + [InlineData(0, "אפס")] + [InlineData(1, "אחת")] + [InlineData(2, "שתיים")] + [InlineData(3, "שלוש")] + [InlineData(4, "ארבע")] + [InlineData(5, "חמש")] + [InlineData(6, "שש")] + [InlineData(7, "שבע")] + [InlineData(8, "שמונה")] + [InlineData(9, "תשע")] + [InlineData(10, "עשר")] + [InlineData(11, "אחת עשרה")] + [InlineData(12, "שתים עשרה")] + [InlineData(19, "תשע עשרה")] + [InlineData(20, "עשרים")] + [InlineData(22, "עשרים ושתיים")] + [InlineData(50, "חמישים")] + [InlineData(99, "תשעים ותשע")] + [InlineData(100, "מאה")] + [InlineData(101, "מאה ואחת")] + [InlineData(111, "מאה ואחת עשרה")] + [InlineData(200, "מאתיים")] + [InlineData(241, "מאתיים ארבעים ואחת")] + [InlineData(500, "חמש מאות")] + [InlineData(505, "חמש מאות וחמש")] + [InlineData(725, "שבע מאות עשרים וחמש")] + [InlineData(1000, "אלף")] + [InlineData(1009, "אלף ותשע")] + [InlineData(1011, "אלף ואחת עשרה")] + [InlineData(1024, "אלף עשרים וארבע")] + [InlineData(1040, "אלף ארבעים")] + [InlineData(2000, "אלפיים")] + [InlineData(7021, "שבעת אלפים עשרים ואחת")] + [InlineData(20000, "עשרים אלף")] + [InlineData(28123, "עשרים ושמונה אלף מאה עשרים ושלוש")] + [InlineData(500000, "חמש מאות אלף")] + [InlineData(500001, "חמש מאות אלף ואחת")] + [InlineData(1000000, "מיליון")] + [InlineData(1000001, "מיליון ואחת")] + [InlineData(2000408, "שני מיליון ארבע מאות ושמונה")] + [InlineData(1000000000, "מיליארד")] + [InlineData(1000000001, "מיליארד ואחת")] + [InlineData(int.MaxValue /* 2147483647 */, "שני מיליארד מאה ארבעים ושבעה מיליון ארבע מאות שמונים ושלוש אלף שש מאות ארבעים ושבע")] + public void ToWords(int number, string expected) + { + Assert.Equal(expected, number.ToWords()); + } + + [Theory] + [InlineData(0, "אפס")] + [InlineData(1, "אחד")] + [InlineData(2, "שניים")] + [InlineData(3, "שלושה")] + [InlineData(4, "ארבעה")] + [InlineData(5, "חמישה")] + [InlineData(6, "שישה")] + [InlineData(7, "שבעה")] + [InlineData(8, "שמונה")] + [InlineData(9, "תשעה")] + [InlineData(10, "עשרה")] + [InlineData(11, "אחד עשר")] + [InlineData(12, "שנים עשר")] + [InlineData(19, "תשעה עשר")] + [InlineData(20, "עשרים")] + [InlineData(22, "עשרים ושניים")] + [InlineData(50, "חמישים")] + [InlineData(99, "תשעים ותשעה")] + [InlineData(100, "מאה")] + [InlineData(101, "מאה ואחד")] + [InlineData(111, "מאה ואחד עשר")] + [InlineData(200, "מאתיים")] + [InlineData(241, "מאתיים ארבעים ואחד")] + [InlineData(500, "חמש מאות")] + [InlineData(505, "חמש מאות וחמישה")] + [InlineData(725, "שבע מאות עשרים וחמישה")] + [InlineData(1000, "אלף")] + [InlineData(1009, "אלף ותשעה")] + [InlineData(1011, "אלף ואחד עשר")] + [InlineData(1024, "אלף עשרים וארבעה")] + [InlineData(1040, "אלף ארבעים")] + [InlineData(2000, "אלפיים")] + [InlineData(7021, "שבעת אלפים עשרים ואחד")] + [InlineData(20000, "עשרים אלף")] + [InlineData(28123, "עשרים ושמונה אלף מאה עשרים ושלושה")] + [InlineData(500000, "חמש מאות אלף")] + [InlineData(500001, "חמש מאות אלף ואחד")] + [InlineData(1000000, "מיליון")] + [InlineData(1000001, "מיליון ואחד")] + [InlineData(2000408, "שני מיליון ארבע מאות ושמונה")] + [InlineData(1000000000, "מיליארד")] + [InlineData(1000000001, "מיליארד ואחד")] + [InlineData(int.MaxValue /* 2147483647 */, "שני מיליארד מאה ארבעים ושבעה מיליון ארבע מאות שמונים ושלוש אלף שש מאות ארבעים ושבעה")] + public void ToWordsMasculine(int number, string expected) + { + Assert.Equal(expected, number.ToWords(GrammaticalGender.Masculine)); + } + + [Theory] + [InlineData(-2, "מינוס שתיים")] + public void NegativeToWords(int number, string expected) + { + Assert.Equal(expected, number.ToWords()); + } + } +} \ No newline at end of file diff --git a/src/Humanizer.sln.DotSettings b/src/Humanizer.sln.DotSettings new file mode 100644 index 000000000..59857493a --- /dev/null +++ b/src/Humanizer.sln.DotSettings @@ -0,0 +1,2 @@ + + ONLY_FOR_MULTILINE \ No newline at end of file diff --git a/src/Humanizer/Humanizer.csproj b/src/Humanizer/Humanizer.csproj index 2747348e3..1ee5c7b3d 100644 --- a/src/Humanizer/Humanizer.csproj +++ b/src/Humanizer/Humanizer.csproj @@ -81,6 +81,7 @@ + diff --git a/src/Humanizer/Localisation/NumberToWords/HebrewNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/HebrewNumberToWordsConverter.cs new file mode 100644 index 000000000..759b158de --- /dev/null +++ b/src/Humanizer/Localisation/NumberToWords/HebrewNumberToWordsConverter.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; + +namespace Humanizer.Localisation.NumberToWords +{ + internal class HebrewNumberToWordsConverter : DefaultNumberToWordsConverter + { + private static readonly string[] UnitsFeminine = { "אפס", "אחת", "שתיים", "שלוש", "ארבע", "חמש", "שש", "שבע", "שמונה", "תשע", "עשר" }; + private static readonly string[] UnitsMasculine = { "אפס", "אחד", "שניים", "שלושה", "ארבעה", "חמישה", "שישה", "שבעה", "שמונה", "תשעה", "עשרה" }; + private static readonly string[] TensUnit = { "עשר", "עשרים", "שלושים", "ארבעים", "חמישים", "שישים", "שבעים", "שמונים", "תשעים" }; + + private class DescriptionAttribute : Attribute + { + public string Description { get; set; } + + public DescriptionAttribute(string description) + { + Description = description; + } + } + + private enum Group + { + Hundreds = 100, + Thousands = 1000, + [Description("מיליון")] + Millions = 1000000, + [Description("מיליארד")] + Billions = 1000000000 + } + + public override string Convert(int number) + { + // in Hebrew, the default number gender form is feminine. + return Convert(number, GrammaticalGender.Feminine); + } + + public override string Convert(int number, GrammaticalGender gender) + { + if (number < 0) + return string.Format("מינוס {0}", Convert(-number, gender)); + + if (number == 0) + return UnitsFeminine[0]; + + var parts = new List(); + if (number >= (int)Group.Billions) + { + ToBigNumber(number, Group.Billions, parts); + number %= (int)Group.Billions; + } + + if (number >= (int)Group.Millions) + { + ToBigNumber(number, Group.Millions, parts); + number %= (int)Group.Millions; + } + + if (number >= (int)Group.Thousands) + { + ToThousands(number, parts); + number %= (int)Group.Thousands; + } + + if (number >= (int)Group.Hundreds) + { + ToHundreds(number, parts); + number %= (int)Group.Hundreds; + } + + if (number > 0) + { + bool appendAnd = parts.Count != 0; + + if (number <= 10) + { + string unit = gender == GrammaticalGender.Masculine ? UnitsMasculine[number] : UnitsFeminine[number]; + if (appendAnd) + unit = "ו" + unit; + parts.Add(unit); + } + else if (number < 20) + { + string unit = Convert(number % 10, gender); + unit = unit.Replace("יי", "י"); + unit = string.Format("{0} {1}", unit, gender == GrammaticalGender.Masculine ? "עשר" : "עשרה"); + if (appendAnd) + unit = "ו" + unit; + parts.Add(unit); + } + else + { + string tenUnit = TensUnit[number / 10 - 1]; + if (number % 10 == 0) + parts.Add(tenUnit); + else + { + string unit = Convert(number % 10, gender); + parts.Add(string.Format("{0} ו{1}", tenUnit, unit)); + } + } + } + + return string.Join(" ", parts); + } + + private void ToBigNumber(int number, Group group, List parts) + { + // Big numbers (million and above) always use the masculine form + // See https://www.safa-ivrit.org/dikduk/numbers.php + + int digits = number / (int)@group; + if (digits == 2) + parts.Add("שני"); + else if (digits > 2) + parts.Add(Convert(digits, GrammaticalGender.Masculine)); + parts.Add(@group.Humanize()); + } + + private void ToThousands(int number, List parts) + { + int thousands = number / (int)Group.Thousands; + + if (thousands == 1) + parts.Add("אלף"); + else if (thousands == 2) + parts.Add("אלפיים"); + else if (thousands <= 10) + parts.Add(UnitsFeminine[thousands] + "ת" + " אלפים"); + else + parts.Add(Convert(thousands) + " אלף"); + } + + private void ToHundreds(int number, List parts) + { + // For hundreds, Hebrew is using the feminine form + // See https://www.safa-ivrit.org/dikduk/numbers.php + + int hundreds = number / (int)Group.Hundreds; + + if (hundreds == 1) + parts.Add("מאה"); + else if (hundreds == 2) + parts.Add("מאתיים"); + else + parts.Add(UnitsFeminine[hundreds] + " מאות"); + } + } +} \ No newline at end of file diff --git a/src/Humanizer/NumberToWordsExtension.cs b/src/Humanizer/NumberToWordsExtension.cs index eae615f55..4f6c531bb 100644 --- a/src/Humanizer/NumberToWordsExtension.cs +++ b/src/Humanizer/NumberToWordsExtension.cs @@ -21,7 +21,8 @@ public static class NumberToWordsExtension {"pt-BR", () => new BrazilianPortugueseNumberToWordsConverter()}, {"ru", () => new RussianNumberToWordsConverter()}, {"fr", () => new FrenchNumberToWordsConverter()}, - {"nl", () => new DutchNumberToWordsConverter()} + {"nl", () => new DutchNumberToWordsConverter()}, + {"he", () => new HebrewNumberToWordsConverter()} }; /// @@ -35,12 +36,23 @@ public static string ToWords(this int number) } /// - /// for Russian locale - /// 1.ToWords(GrammaticalGender.Masculine) -> "один" - /// 1.ToWords(GrammaticalGender.Feminine) -> "одна" + /// For locales that support gender-specific forms /// + /// + /// Russian: + /// + /// 1.ToWords(GrammaticalGender.Masculine) -> "один" + /// 1.ToWords(GrammaticalGender.Feminine) -> "одна" + /// + /// Hebrew: + /// + /// 1.ToWords(GrammaticalGender.Masculine) -> "אחד" + /// 1.ToWords(GrammaticalGender.Feminine) -> "אחת" + /// + /// + /// /// Number to be turned to words - /// The grammatical gender to use for output words. Defaults to masculine. + /// The grammatical gender to use for output words /// public static string ToWords(this int number, GrammaticalGender gender) {