Skip to content

Commit

Permalink
Closes #369
Browse files Browse the repository at this point in the history
PluralRule for DualFromZeroToTwo: Does not cover value of 2

The `Dictionary<string, PluralRuleDelegate> IsoLangToDelegate` is backed by a default dictionary. It can be restored with `PluralRules.RestoreDefault()` if one of the delegates was changed. Both has global effect.

Extract class `CustomPluralRuleProvider` to its own file

`PluralRuleDelegate DualFromZeroToTwo`:
* with 3 words, the index is for counts of 0, > 0 and < 2, more than 2
* with 4 words, the index is for counts of 0, > 0 and < 2,  >= 2 and < 3, more than 3

Add unit tests
  • Loading branch information
axunonb committed Jan 8, 2024
1 parent f89ccf9 commit 69916e3
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 91 deletions.
154 changes: 136 additions & 18 deletions src/SmartFormat.Tests/Extensions/PluralLocalizationFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using NUnit.Framework;
Expand All @@ -23,12 +24,12 @@ private static SmartFormatter GetFormatter(SmartSettings? smartSettings = null)

private static void TestAllResults(CultureInfo cultureInfo, string format, ExpectedResults expectedValuesAndResults)
{
foreach (var test in expectedValuesAndResults)
foreach (var testResult in expectedValuesAndResults)
{
var smart = GetFormatter();
var value = test.Key;
var expected = test.Value;
var actual = smart.Format(cultureInfo, format, value);
var count = testResult.Key;
var expected = testResult.Value;
var actual = smart.Format(cultureInfo, format, count);

Assert.That(actual, Is.EqualTo(expected));
Debug.WriteLine(actual);
Expand Down Expand Up @@ -153,6 +154,45 @@ public void Test_English_Unsigned()
}
}

[TestCase(0, "{0} personne")] // 0 is singular
[TestCase(1, "{0} personne")] // 1 is singular
[TestCase(2, "{0} personnes")] // 2 or more is plural
[TestCase(50, "{0} personnes")]
public void Test_French_2words(int count, string expected)
{
var smart = GetFormatter();
var ci = CultureInfo.GetCultureInfo("fr");
var actual = smart.Format(ci, "{0:plural:{0} personne|{0} personnes}", count);

Assert.That(actual, Is.EqualTo(string.Format(ci, expected, count)));
}

[TestCase(0, "pas de personne")] // 0 is singular
[TestCase(1, "une personne")] // 1 is singular
[TestCase(2, "{0} personnes")] // 2 or more is plural
[TestCase(50, "{0} personnes")]
public void Test_French_3words(int count, string expected)
{
var smart = GetFormatter();
var ci = CultureInfo.GetCultureInfo("fr");
var actual = smart.Format(ci, "{0:plural:pas de personne|une personne|{0} personnes}", count);

Assert.That(actual, Is.EqualTo(string.Format(ci, expected, count)));
}

[TestCase(0, "pas de personne")] // 0 is singular
[TestCase(1, "une personne")] // 1 is singular
[TestCase(2, "deux personnes")] // 2 is plural
[TestCase(50, "{0} personnes")] // more than 2
public void Test_French_4words(int count, string expected)
{
var smart = GetFormatter();
var ci = CultureInfo.GetCultureInfo("fr");
var actual = smart.Format(ci, "{0:plural:pas de personne|une personne|deux personnes|{0} personnes}", count);

Assert.That(actual, Is.EqualTo(string.Format(ci, expected, count)));
}

[Test]
public void Test_Turkish()
{
Expand Down Expand Up @@ -273,7 +313,7 @@ public void NamedFormatter_should_use_specific_language(string format, object ar
[TestCase("{0:plural:zero|one|many}", new string[0], "zero")]
[TestCase("{0:plural:zero|one|many}", new[] { "alice" }, "one")]
[TestCase("{0:plural:zero|one|many}", new[] { "alice", "bob" }, "many")]
public void Test_should_allow_ienumerable_parameter(string format, object arg0, string expectedResult)
public void Should_Allow_IEnumerable_Parameter(string format, object arg0, string expectedResult)
{
var smart = GetFormatter();
var culture = new CultureInfo("en-US");
Expand All @@ -282,23 +322,54 @@ public void Test_should_allow_ienumerable_parameter(string format, object arg0,
}

[Test]
public void Test_With_CustomPluralRuleProvider()
public void Use_CustomPluralRuleProvider()
{
var smart = GetFormatter();
var actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("de")), "{0:plural:Frau|Frauen}", new string[2], "more");
Assert.That(actualResult, Is.EqualTo("Frauen"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("en")), "{0:plural:person|people}", new string[2], "more");
Assert.That(actualResult, Is.EqualTo("people"));
Assert.Multiple(() =>
{
// ** German **
var actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("de")),
"{0:plural:Frau|Frauen}", new string[2], "more");
Assert.That(actualResult, Is.EqualTo("Frauen"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("de")),
"{0:plural:Frau|Frauen|einige Frauen|viele Frauen}", new string[4], "more");
Assert.That(actualResult, Is.EqualTo("viele Frauen"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("en")), "{0:plural:person|people}", new string[1], "one");
Assert.That(actualResult, Is.EqualTo("person"));
// ** English **

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")), "{0:plural:une personne|deux personnes|plusieurs personnes}", new string[3], "several");
Assert.That(actualResult, Is.EqualTo("plusieurs personnes"));
actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("en")),
"{0:plural:person|people}", new string[2], "more");
Assert.That(actualResult, Is.EqualTo("people"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")), "{0:plural:une personne|deux personnes|plusieurs personnes|beaucoup de personnes}", new string[3], "several");
Assert.That(actualResult, Is.EqualTo("beaucoup de personnes"));
actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("en")),
"{0:plural:person|people}", new string[1], "one");
Assert.That(actualResult, Is.EqualTo("person"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")),
"{0:plural:pas de personne|une personne|plusieurs personnes}", Array.Empty<string>(), "none");
Assert.That(actualResult, Is.EqualTo("pas de personne"));

// ** French **

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")),
"{0:plural:pas de personne|une personne|plusieurs personnes}", new string[1], "one");
Assert.That(actualResult, Is.EqualTo("une personne"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")),
"{0:plural:pas de personne|une personne|deux personnes}", new string[2], "two");
Assert.That(actualResult, Is.EqualTo("deux personnes"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")),
"{0:plural:pas de personne|une personne|deux personnes|plusieurs personnes}", new string[3], "several");
Assert.That(actualResult, Is.EqualTo("plusieurs personnes"));

actualResult = smart.Format(new CustomPluralRuleProvider(PluralRules.GetPluralRule("fr")),
"{0:plural:une personne|deux personnes|plusieurs personnes|beaucoup de personnes}", new string[3],
"many");
Assert.That(actualResult, Is.EqualTo("beaucoup de personnes"));
});
}

[TestCase("{0:plural:one|many} {1:plural:one|many} {2:plural:one|many}", "many one many")]
Expand All @@ -318,7 +389,10 @@ public void Should_Process_Signed_And_Unsigned_Numbers()
{
var smart = GetFormatter();
foreach (var number in new object[]
{ (long)123, (ulong)123, (short)123, (ushort)123, (int)123, (uint)123 })
{
(long)123, (ulong)123, (short)123, (ushort)123, (int)123, (uint)123,
(long)-123, (short) -123, (int) -123
})
{
Assert.That(smart.Format("{0:plural(en):zero|one|many}", number), Is.EqualTo("many"));
}
Expand Down Expand Up @@ -363,4 +437,48 @@ public void Pluralization_With_Changed_SplitChar(int numOfPeople, string format,
var result = smart.Format(format, data);
Assert.That(result, Is.EqualTo(expected));
}

[TestCase(0, "nobody", "pas de personne")] // 0 is singular
[TestCase(1, "{0} person", "{0} personne")] // 1 is singular
[TestCase(2, "{0} people", "{0} personnes")] // 2 or more is plural
[TestCase(5, "a couple of people", "quelques personnes")]
[TestCase(15, "many people", "beaucoup de gens")]
[TestCase(50, "a lot of people", "une foule de personnes")]
public void Pluralization_With_Changed_Default_Rule_Delegate(int count, string rawExpectedEnglish, string rawExpectedFrench)
{
// Note: This test changes a default rule delegate *globally*.
// It is not recommended, but possible.
PluralRules.IsoLangToDelegate["en"] = (value, wordsCount) =>
{
if (wordsCount != 6) return -1;

return Math.Abs(value) switch
{
<= 0 => 0,
> 0 and < 2 => 1,
>= 2 and < 3 => 2,
> 2 and < 10 => 3,
>= 10 and < 20 => 4,
>= 20 => 5
};
};
// Use the same rule delegate for English and French:
PluralRules.IsoLangToDelegate["fr"] = PluralRules.IsoLangToDelegate["en"];

var smart = GetFormatter();
var ciEnglish = CultureInfo.GetCultureInfo("en");
var ciFrench = CultureInfo.GetCultureInfo("fr");

var actualEnglish = smart.Format(ciEnglish, "{0:plural:nobody|{} person|{} people|a couple of people|many people|a lot of people}", count);
var actualFrench = smart.Format(ciFrench, "{0:plural:pas de personne|{} personne|{} personnes|quelques personnes|beaucoup de gens|une foule de personnes}", count);

// Restore default rule delegates:
PluralRules.RestoreDefault();

Assert.Multiple(() =>
{
Assert.That(actualEnglish, Is.EqualTo(string.Format(ciEnglish, rawExpectedEnglish, count)));
Assert.That(actualFrench, Is.EqualTo(string.Format(ciFrench, rawExpectedFrench, count)));
});
}
}
44 changes: 44 additions & 0 deletions src/SmartFormat/Extensions/CustomPluralRuleProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// Copyright SmartFormat Project maintainers and contributors.
// Licensed under the MIT license.
//

using System;
using SmartFormat.Utilities;

namespace SmartFormat.Extensions;
/// <summary>
/// Use this class to provide custom plural rules to Smart.Format
/// </summary>
public class CustomPluralRuleProvider : IFormatProvider
{
private readonly PluralRules.PluralRuleDelegate _pluralRule;

/// <summary>
/// Creates a new instance of a <see cref="CustomPluralRuleProvider"/>.
/// </summary>
/// <param name="pluralRule">The delegate for plural rules.</param>
public CustomPluralRuleProvider(PluralRules.PluralRuleDelegate pluralRule)
{
_pluralRule = pluralRule;
}

/// <summary>
/// Gets the format <see cref="object"/> for a <see cref="CustomPluralRuleProvider"/>.
/// </summary>
/// <param name="formatType"></param>
/// <returns>The format <see cref="object"/> for a <see cref="CustomPluralRuleProvider"/> or <see langword="null"/>.</returns>
public object? GetFormat(Type? formatType)
{
return formatType == typeof(CustomPluralRuleProvider) ? this : default;
}

/// <summary>
/// Gets the <see cref="PluralRules.PluralRuleDelegate"/> of the current <see cref="CustomPluralRuleProvider"/> instance.
/// </summary>
/// <returns></returns>
public PluralRules.PluralRuleDelegate GetPluralRule()
{
return _pluralRule;
}
}
35 changes: 0 additions & 35 deletions src/SmartFormat/Extensions/PluralLocalizationFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,38 +222,3 @@ private static CultureInfo GetCultureInfo(IFormattingInfo formattingInfo)
}
}

/// <summary>
/// Use this class to provide custom plural rules to Smart.Format
/// </summary>
public class CustomPluralRuleProvider : IFormatProvider
{
private readonly PluralRules.PluralRuleDelegate _pluralRule;

/// <summary>
/// Creates a new instance of a <see cref="CustomPluralRuleProvider"/>.
/// </summary>
/// <param name="pluralRule">The delegate for plural rules.</param>
public CustomPluralRuleProvider(PluralRules.PluralRuleDelegate pluralRule)
{
_pluralRule = pluralRule;
}

/// <summary>
/// Gets the format <see cref="object"/> for a <see cref="CustomPluralRuleProvider"/>.
/// </summary>
/// <param name="formatType"></param>
/// <returns>The format <see cref="object"/> for a <see cref="CustomPluralRuleProvider"/> or <see langword="null"/>.</returns>
public object? GetFormat(Type? formatType)
{
return formatType == typeof(CustomPluralRuleProvider) ? this : default;
}

/// <summary>
/// Gets the <see cref="PluralRules.PluralRuleDelegate"/> of the current <see cref="CustomPluralRuleProvider"/> instance.
/// </summary>
/// <returns></returns>
public PluralRules.PluralRuleDelegate GetPluralRule()
{
return _pluralRule;
}
}
Loading

0 comments on commit 69916e3

Please sign in to comment.