diff --git a/src/Humanizer.Tests/Localisation/bg/NumberToWordsTests.cs b/src/Humanizer.Tests/Localisation/bg/NumberToWordsTests.cs index 087402b21..c4c38dabe 100644 --- a/src/Humanizer.Tests/Localisation/bg/NumberToWordsTests.cs +++ b/src/Humanizer.Tests/Localisation/bg/NumberToWordsTests.cs @@ -37,15 +37,21 @@ public class NumberToWordsTests [InlineData(111, "сто и единадесет")] [InlineData(55, "петдесет и пет")] [InlineData(555, "петстотин петдесет и пет")] + [InlineData(1000, "една хиляда")] + [InlineData(2000, "две хиляди")] [InlineData(4213, "четири хиляди двеста и тринадесет")] [InlineData(5000, "пет хиляди")] [InlineData(28205, "двадесет и осем хиляди двеста и пет")] [InlineData(35000, "тридесет и пет хиляди")] [InlineData(352192, "триста петдесет и две хиляди сто деветдесет и две")] + [InlineData(1000000, "един милион")] + [InlineData(2000000, "два милиона")] [InlineData(4000210, "четири милиона двеста и десет")] [InlineData(5200, "пет хиляди и двеста")] [InlineData(1125000, "един милион и сто двадесет и пет хиляди")] [InlineData(1000000000, "един милиард")] + [InlineData(2000000000, "два милиарда")] + [InlineData(3000000000, "три милиарда")] public void ToWordsBg(long number, string expected) => Assert.Equal(expected, number.ToWords()); @@ -79,6 +85,19 @@ public void ToWordsBg(long number, string expected) => [InlineData(21, "двадесет и първи")] [InlineData(22, "двадесет и втори")] [InlineData(35, "тридесет и пети")] + [InlineData(100, "стотен")] + [InlineData(111, "сто и единадесети")] + [InlineData(200, "двестотен")] + [InlineData(300, "тристотен")] + [InlineData(1000, "една хиляден")] + [InlineData(1111, "една хиляда сто и единадесети")] + [InlineData(10000, "десет хиляден")] + [InlineData(12345, "дванадесет хиляди триста четиридесет и пети")] + [InlineData(12000, "дванадесет хиляден")] + [InlineData(100000, "сто хиляден")] + [InlineData(101111, "сто и една хиляда сто и единадесети")] + [InlineData(1000000, "един милионен")] + [InlineData(2000000, "два милионен")] public void ToOrdinalWordsMasculine(int number, string expected) => Assert.Equal(expected, number.ToOrdinalWords(GrammaticalGender.Masculine)); @@ -112,6 +131,19 @@ public void ToOrdinalWordsMasculine(int number, string expected) => [InlineData(21, "двадесет и първа")] [InlineData(22, "двадесет и втора")] [InlineData(35, "тридесет и пета")] + [InlineData(100, "стотна")] + [InlineData(111, "сто и единадесета")] + [InlineData(200, "двестотна")] + [InlineData(300, "тристотна")] + [InlineData(1000, "една хилядна")] + [InlineData(1111, "една хиляда сто и единадесета")] + [InlineData(10000, "десет хилядна")] + [InlineData(12345, "дванадесет хиляди триста четиридесет и пета")] + [InlineData(12000, "дванадесет хилядна")] + [InlineData(100000, "сто хилядна")] + [InlineData(101111, "сто и една хиляда сто и единадесета")] + [InlineData(1000000, "един милионна")] + [InlineData(2000000, "два милионна")] public void ToOrdinalWordsFeminine(int number, string expected) => Assert.Equal(expected, number.ToOrdinalWords(GrammaticalGender.Feminine)); @@ -145,6 +177,19 @@ public void ToOrdinalWordsFeminine(int number, string expected) => [InlineData(21, "двадесет и първо")] [InlineData(22, "двадесет и второ")] [InlineData(35, "тридесет и пето")] + [InlineData(100, "стотно")] + [InlineData(111, "сто и единадесето")] + [InlineData(200, "двестотно")] + [InlineData(300, "тристотно")] + [InlineData(1000, "една хилядно")] + [InlineData(1111, "една хиляда сто и единадесето")] + [InlineData(10000, "десет хилядно")] + [InlineData(12345, "дванадесет хиляди триста четиридесет и пето")] + [InlineData(12000, "дванадесет хилядно")] + [InlineData(100000, "сто хилядно")] + [InlineData(101111, "сто и една хиляда сто и единадесето")] + [InlineData(1000000, "един милионно")] + [InlineData(2000000, "два милионно")] public void ToOrdinalWordsNeuter(int number, string expected) => Assert.Equal(expected, number.ToOrdinalWords(GrammaticalGender.Neuter)); } \ No newline at end of file diff --git a/src/Humanizer.Tests/Localisation/bg/TimeSpanHumanizeTests.cs b/src/Humanizer.Tests/Localisation/bg/TimeSpanHumanizeTests.cs index 2471952f9..0964e1683 100644 --- a/src/Humanizer.Tests/Localisation/bg/TimeSpanHumanizeTests.cs +++ b/src/Humanizer.Tests/Localisation/bg/TimeSpanHumanizeTests.cs @@ -5,7 +5,7 @@ public class TimeSpanHumanizeTests { [Theory] [Trait("Translation", "Google")] - [InlineData(366, "една година")] + [InlineData(366, "1 година")] [InlineData(731, "2 години")] [InlineData(1096, "3 години")] [InlineData(4018, "11 години")] @@ -14,7 +14,17 @@ public void Years(int days, string expected) => [Theory] [Trait("Translation", "Google")] - [InlineData(31, "един месец")] + [InlineData(366, "една година")] + [InlineData(731, "две години")] + [InlineData(1096, "три години")] + [InlineData(4018, "единадесет години")] + // [InlineData(7671, "двадесет и една година")] + public void YearsToWords(int days, string expected) => + Assert.Equal(expected, TimeSpan.FromDays(days).Humanize(maxUnit: TimeUnit.Year, toWords: true)); + + [Theory] + [Trait("Translation", "Google")] + [InlineData(31, "1 месец")] [InlineData(61, "2 месеца")] [InlineData(92, "3 месеца")] [InlineData(335, "11 месеца")] @@ -22,41 +32,86 @@ public void Months(int days, string expected) => Assert.Equal(expected, TimeSpan.FromDays(days).Humanize(maxUnit: TimeUnit.Year)); [Theory] - [InlineData(7, "една седмица")] + [Trait("Translation", "Google")] + [InlineData(31, "един месец")] + [InlineData(61, "два месеца")] + [InlineData(92, "три месеца")] + [InlineData(335, "единадесет месеца")] + public void MonthsToWords(int days, string expected) => + Assert.Equal(expected, TimeSpan.FromDays(days).Humanize(maxUnit: TimeUnit.Month, toWords: true)); + + [Theory] + [InlineData(7, "1 седмица")] [InlineData(14, "2 седмици")] public void Weeks(int days, string expected) => Assert.Equal(expected, TimeSpan.FromDays(days).Humanize()); [Theory] - [InlineData(1, "един ден")] + [InlineData(7, "една седмица")] + [InlineData(14, "две седмици")] + public void WeeksToWords(int days, string expected) => + Assert.Equal(expected, TimeSpan.FromDays(days).Humanize(toWords: true)); + + [Theory] + [InlineData(1, "1 ден")] [InlineData(2, "2 дена")] public void Days(int days, string expected) => Assert.Equal(expected, TimeSpan.FromDays(days).Humanize()); [Theory] - [InlineData(1, "един час")] + [InlineData(1, "един ден")] + [InlineData(2, "два дена")] + public void DaysToWords(int days, string expected) => + Assert.Equal(expected, TimeSpan.FromDays(days).Humanize(toWords: true)); + + [Theory] + [InlineData(1, "1 час")] [InlineData(2, "2 часа")] public void Hours(int hours, string expected) => Assert.Equal(expected, TimeSpan.FromHours(hours).Humanize()); [Theory] - [InlineData(1, "една минута")] + [InlineData(1, "един час")] + [InlineData(2, "два часа")] + public void HoursToWords(int hours, string expected) => + Assert.Equal(expected, TimeSpan.FromHours(hours).Humanize(toWords: true)); + + [Theory] + [InlineData(1, "1 минута")] [InlineData(2, "2 минути")] public void Minutes(int minutes, string expected) => Assert.Equal(expected, TimeSpan.FromMinutes(minutes).Humanize()); [Theory] - [InlineData(1, "една секунда")] + [InlineData(1, "една минута")] + [InlineData(2, "две минути")] + public void MinutesToWords(int minutes, string expected) => + Assert.Equal(expected, TimeSpan.FromMinutes(minutes).Humanize(toWords: true)); + + [Theory] + [InlineData(1, "1 секунда")] [InlineData(2, "2 секунди")] public void Seconds(int seconds, string expected) => Assert.Equal(expected, TimeSpan.FromSeconds(seconds).Humanize()); [Theory] - [InlineData(1, "една милисекунда")] + [InlineData(1, "една секунда")] + [InlineData(2, "две секунди")] + public void SecondsToWords(int seconds, string expected) => + Assert.Equal(expected, TimeSpan.FromSeconds(seconds).Humanize(toWords: true)); + + [Theory] + [InlineData(1, "1 милисекунда")] [InlineData(2, "2 милисекунди")] public void Milliseconds(int milliseconds, string expected) => Assert.Equal(expected, TimeSpan.FromMilliseconds(milliseconds).Humanize()); + [Theory] + [InlineData(1, "една милисекунда")] + [InlineData(2, "две милисекунди")] + public void MillisecondsToWords(int milliseconds, string expected) => + Assert.Equal(expected, TimeSpan.FromMilliseconds(milliseconds).Humanize(toWords: true)); + [Fact] public void NoTime() => // This one doesn't make a lot of sense but ... w/e diff --git a/src/Humanizer/Configuration/FormatterRegistry.cs b/src/Humanizer/Configuration/FormatterRegistry.cs index 005db2f16..6297b8296 100644 --- a/src/Humanizer/Configuration/FormatterRegistry.cs +++ b/src/Humanizer/Configuration/FormatterRegistry.cs @@ -20,7 +20,7 @@ public FormatterRegistry() RegisterCzechSlovakPolishFormatter("cs"); RegisterCzechSlovakPolishFormatter("pl"); RegisterCzechSlovakPolishFormatter("sk"); - RegisterDefaultFormatter("bg"); + Register("bg", new BulgarianFormatter()); RegisterDefaultFormatter("ku"); RegisterDefaultFormatter("pt"); RegisterDefaultFormatter("sv"); diff --git a/src/Humanizer/Localisation/Formatters/BulgarianFormatter.cs b/src/Humanizer/Localisation/Formatters/BulgarianFormatter.cs new file mode 100644 index 000000000..9e13582f5 --- /dev/null +++ b/src/Humanizer/Localisation/Formatters/BulgarianFormatter.cs @@ -0,0 +1,14 @@ +namespace Humanizer; + +class BulgarianFormatter() : DefaultFormatter("bg") +{ + protected override string NumberToWords(TimeUnit unit, int number, CultureInfo culture) => + number.ToWords(GetUnitGender(unit), culture); + + static GrammaticalGender GetUnitGender(TimeUnit unit) => + unit switch + { + TimeUnit.Hour or TimeUnit.Day or TimeUnit.Month => GrammaticalGender.Masculine, + _ => GrammaticalGender.Feminine + }; +} \ No newline at end of file diff --git a/src/Humanizer/Localisation/NumberToWords/BulgarianNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/BulgarianNumberToWordsConverter.cs index 30a9d5914..8f0a18718 100644 --- a/src/Humanizer/Localisation/NumberToWords/BulgarianNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/BulgarianNumberToWordsConverter.cs @@ -1,6 +1,7 @@ namespace Humanizer; -class BulgarianNumberToWordsConverter : GenderedNumberToWordsConverter +class BulgarianNumberToWordsConverter() : + GenderedNumberToWordsConverter(GrammaticalGender.Neuter) { static readonly string[] UnitsMap = [ @@ -23,7 +24,7 @@ class BulgarianNumberToWordsConverter : GenderedNumberToWordsConverter static readonly string[] HundredsOrdinalMap = [ - string.Empty, "стот", "двест", "трист", "четиристот", "петстот", "шестстот", "седемстот", "осемстот", + string.Empty, "стот", "двестот", "тристот", "четиристот", "петстот", "шестстот", "седемстот", "осемстот", "деветстот" ]; @@ -37,137 +38,147 @@ class BulgarianNumberToWordsConverter : GenderedNumberToWordsConverter public override string Convert(long input, GrammaticalGender gender, bool addAnd = true) => InnerConvert(input, gender, false); + public override string ConvertToOrdinal(int input, GrammaticalGender gender) => + InnerConvert(input, gender, true); + static string InnerConvert(long input, GrammaticalGender gender, bool isOrdinal) { - if (input is > int.MaxValue or < int.MinValue) - { - throw new NotImplementedException(); - } - if (input == 0) { - return isOrdinal ? "нулев" + GetEndingForGender(gender, input) : "нула"; + return isOrdinal ? OrdinalZero(gender) : "нула"; } var parts = new List(); - if (input < 0) { parts.Add("минус"); input = -input; } - var lastOrdinalSubstitution = ""; + CollectParts(parts, ref input, isOrdinal, 1_000_000_000_000_000_000, GrammaticalGender.Masculine, "квинтилион", "квадрилиона", ToOrdinalOverAHundred("квинтилион", gender)); + CollectParts(parts, ref input, isOrdinal, 1_000_000_000_000_000, GrammaticalGender.Masculine, "квадрилион", "квадрилиона", ToOrdinalOverAHundred("квадрилион", gender)); + CollectParts(parts, ref input, isOrdinal, 1_000_000_000_000, GrammaticalGender.Masculine, "трилион", "трилиона", ToOrdinalOverAHundred("трилион", gender)); + CollectParts(parts, ref input, isOrdinal, 1_000_000_000, GrammaticalGender.Masculine, "милиард", "милиарда", ToOrdinalOverAHundred("милиард", gender)); + CollectParts(parts, ref input, isOrdinal, 1_000_000, GrammaticalGender.Masculine, "милион", "милиона", ToOrdinalOverAHundred("милион", gender)); + CollectParts(parts, ref input, isOrdinal, 1_000, GrammaticalGender.Feminine, "хиляда", "хиляди", ToOrdinalOverAHundred("хиляд", gender)); + CollectPartsUnderOneThousand(parts, ref input, isOrdinal, gender); - if (input / 1000000000 > 0) - { - parts.Add(input < 2000000000 ? "един милиард" : InnerConvert(input / 1000000000, gender, false) + " милиарда"); + return string.Join(" ", parts); + } - if (isOrdinal) - lastOrdinalSubstitution = InnerConvert(input / 1000000000, gender, false) + " милиард" + - GetEndingForGender(gender, input); - input %= 1000000000; + static void CollectParts(IList parts, ref long number, bool isOrdinal, long divisor, GrammaticalGender gender, string singular, string plural, string ordinal) + { + if (number < divisor) + { + return; } - if (input / 1000000 > 0) + var result = number / divisor; + + if (parts.Count > 0) { - parts.Add(input < 2000000 ? "един милион" : InnerConvert(input / 1000000, gender, false) + " милиона"); + parts.Add("и"); + } - if (isOrdinal) - lastOrdinalSubstitution = InnerConvert(input / 1000000, gender, false) + " милион" + - GetEndingForGender(gender, input); + CollectPartsUnderOneThousand(parts, ref result, false, gender); - input %= 1000000; + number %= divisor; + if (number == 0 && isOrdinal) + { + parts.Add(ordinal); } - - if (input / 1000 > 0) + else { - if (input < 2000) - parts.Add("хиляда"); - else - { - parts.Add(InnerConvert(input / 1000, gender, false) + " хиляди"); - } - - if (isOrdinal) - lastOrdinalSubstitution = InnerConvert(input / 1000, gender, false) + " хиляд" + - GetEndingForGender(gender, input); - - input %= 1000; + parts.Add(result == 1 ? singular : plural); } + } - if (input / 100 > 0) + static void CollectPartsUnderOneThousand(IList parts, ref long number, bool isOrdinal, GrammaticalGender gender) + { + if (number == 0) { - parts.Add(HundredsMap[(int) input / 100]); - - if (isOrdinal) - lastOrdinalSubstitution = HundredsOrdinalMap[(int) input / 100] + GetEndingForGender(gender, input); - - input %= 100; + return; } - if (input > 19) + if (number >= 100) { - parts.Add(TensMap[input / 10]); - - if (isOrdinal) - lastOrdinalSubstitution = TensMap[(int) input / 10] + GetEndingForGender(gender, input); - - input %= 10; + var hundreds = number / 100; + number %= 100; + if (number == 0 && isOrdinal) + { + parts.Add(ToOrdinalOverAHundred(HundredsOrdinalMap[hundreds], gender)); + } + else + { + parts.Add(HundredsMap[hundreds]); + } } - if (input > 0) + if (number >= 20) { - parts.Add(UnitsMap[input]); + var tens = number / 10; + number %= 10; + if (number == 0 && isOrdinal) + { + parts.Add(ToOrdinalUnitsAndTens(TensMap[tens], gender)); + } + else + { + parts.Add(TensMap[tens]); + } + } + if (number > 0) + { if (isOrdinal) - lastOrdinalSubstitution = UnitsOrdinal[input] + GetEndingForGender(gender, input); + { + parts.Add(ToOrdinalUnitsAndTens(UnitsOrdinal[number], gender)); + } + else + { + parts.Add(GetUnit(number, gender)); + } } if (parts.Count > 1) { parts.Insert(parts.Count - 1, "и"); } - - if (isOrdinal && !string.IsNullOrWhiteSpace(lastOrdinalSubstitution)) - parts[^1] = lastOrdinalSubstitution; - - return string.Join(" ", parts); } - public override string ConvertToOrdinal(int input, GrammaticalGender gender) => - InnerConvert(input, gender, true); + static string GetUnit(long number, GrammaticalGender gender) => + (number, gender) switch + { + (1, GrammaticalGender.Masculine) => "един", + (1, GrammaticalGender.Feminine) => "една", + (2, GrammaticalGender.Masculine) => "два", + _ => UnitsMap[number], + }; - static string GetEndingForGender(GrammaticalGender gender, long input) - { - if (input == 0) + static string OrdinalZero(GrammaticalGender gender) => + gender switch { - return gender switch - { - GrammaticalGender.Masculine => "", - GrammaticalGender.Feminine => "а", - GrammaticalGender.Neuter => "о", - _ => throw new ArgumentOutOfRangeException(nameof(gender)) - }; - } + GrammaticalGender.Masculine => "нулев", + GrammaticalGender.Feminine => "нулева", + GrammaticalGender.Neuter => "нулево", + _ => throw new ArgumentOutOfRangeException(nameof(gender), gender, null) + }; - if (input < 99) + static string ToOrdinalOverAHundred(string word, GrammaticalGender gender) => + gender switch { - return gender switch - { - GrammaticalGender.Masculine => "и", - GrammaticalGender.Feminine => "а", - GrammaticalGender.Neuter => "о", - _ => throw new ArgumentOutOfRangeException(nameof(gender)) - }; - } + GrammaticalGender.Masculine => $"{word}ен", + GrammaticalGender.Feminine => $"{word}на", + GrammaticalGender.Neuter => $"{word}но", + _ => throw new ArgumentOutOfRangeException(nameof(gender)) + }; - return gender switch + static string ToOrdinalUnitsAndTens(string word, GrammaticalGender gender) => + gender switch { - GrammaticalGender.Masculine => "ен", - GrammaticalGender.Feminine => "на", - GrammaticalGender.Neuter => "но", + GrammaticalGender.Masculine => $"{word}и", + GrammaticalGender.Feminine => $"{word}а", + GrammaticalGender.Neuter => $"{word}о", _ => throw new ArgumentOutOfRangeException(nameof(gender)) }; - } } \ No newline at end of file diff --git a/src/Humanizer/Properties/Resources.bg.resx b/src/Humanizer/Properties/Resources.bg.resx index 104de5a71..035c6a9a8 100644 --- a/src/Humanizer/Properties/Resources.bg.resx +++ b/src/Humanizer/Properties/Resources.bg.resx @@ -168,30 +168,36 @@ {0} секунди - + един ден - + един час - + една милисекунда - + една минута - + една секунда + + една седмица + + + един месец + + + една година + няма време {0} седмици - - една седмица - след {0} дена @@ -237,10 +243,28 @@ {0} години + + 1 ден + + + 1 час + + + 1 милисекунда + + + 1 минута + + + 1 секунда + + + 1 седмица + - един месец + 1 месец - една година + 1 година \ No newline at end of file