diff --git a/CHANGES.md b/CHANGES.md index c5f7b4dc..9b8d4a82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -582,6 +582,15 @@ _ = smart.Format({0:cond:|No|\t|Yes|}", 1); // Result: "|Yes|" ``` +c) ChooseFormatter [#253](https://github.com/axuno/SmartFormat/pull/253) + + Modified `ChooseFormatter` case-sensitivity for option strings. This modification is compatible with v2. + + * `bool` and `null` as string: always case-insensitive + * using `SmartSettings.CaseSensitivity` unless overridden with `ChooseFormatter.CaseSensitivity` + * option strings comparison is culture-aware + + v2.7.2 === * **Fixed**: `ConditionalFormatter` processes unsigned numbers in arguments correctly. diff --git a/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs b/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs index 0ab04d16..58420bd8 100644 --- a/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs @@ -44,15 +44,22 @@ public void Choose_With_Changed_SplitChar() Assert.That(result, Is.EqualTo("|two|")); } - [TestCase("{0:choose(true|True):one|two|default}", true, "two")] - [TestCase("{0:choose(true|TRUE):one|two|default}", true, "default")] - [TestCase("{0:choose(string|String):one|two|default}", "String", "two")] - [TestCase("{0:choose(string|STRING):one|two|default}", "String", "default")] - [TestCase("{0:choose(ignore|Ignore):one|two|default}", SmartFormat.Core.Settings.FormatErrorAction.Ignore, "two")] - [TestCase("{0:choose(ignore|IGNORE):one|two|default}", SmartFormat.Core.Settings.FormatErrorAction.Ignore, "default")] - public void Choose_should_be_case_sensitive(string format, object arg0, string expectedResult) + // bool and null args: always case-insensitive + [TestCase("{0:choose(true|false):one|two|default}", false, true, "one")] + [TestCase("{0:choose(True|FALSE):one|two|default}", false, false, "two")] + [TestCase("{0:choose(null):is null|default}", false, default, "is null")] + [TestCase("{0:choose(NULL):is null|default}", false, default, "is null")] + // strings + [TestCase("{0:choose(string|String):one|two|default}", true, "String", "two")] + [TestCase("{0:choose(string|STRING):one|two|default}", true, "String", "default")] + // Enum + [TestCase("{0:choose(ignore|Ignore):one|two|default}", true, FormatErrorAction.Ignore, "two")] + [TestCase("{0:choose(ignore|IGNORE):one|two|default}", true, FormatErrorAction.Ignore, "default")] + public void Choose_should_be_case_sensitive(string format, bool caseSensitive, object arg0, string expectedResult) { var smart = Smart.CreateDefaultSmartFormat(); + smart.GetFormatterExtension()!.CaseSensitivity = + caseSensitive ? CaseSensitivityType.CaseSensitive : CaseSensitivityType.CaseInsensitive; Assert.AreEqual(expectedResult, smart.Format(format, arg0)); } @@ -133,5 +140,19 @@ public void May_Contain_Nested_Choose_Formats(int? nullableInt, int valueIfNull, Assert.That(result, Is.EqualTo(expected)); } + + [Test, Description("Case-insensitive option string comparison")] + public void Choose_Should_Use_CultureInfo_For_Option_Strings() + { + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; + var smart = Smart.CreateDefaultSmartFormat(); + smart.GetFormatterExtension()!.CaseSensitivity = CaseSensitivityType.CaseInsensitive; + + var result1 = smart.Format(CultureInfo.GetCultureInfo("de"), "{0:choose(ä|ü):umlautA|umlautU}", "Ä"); + var result2 = smart.Format(CultureInfo.GetCultureInfo("de"), "{0:choose(ä|ü):umlautA|umlautU}", "ä"); + + Assert.That(result1, Is.EqualTo("umlautA")); + Assert.That(result2, Is.EqualTo("umlautA")); + } } } diff --git a/src/SmartFormat.Tests/Extensions/LocalizationFormatterTests.cs b/src/SmartFormat.Tests/Extensions/LocalizationFormatterTests.cs index 83726213..fa1ae50e 100644 --- a/src/SmartFormat.Tests/Extensions/LocalizationFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/LocalizationFormatterTests.cs @@ -111,7 +111,7 @@ public void Should_Use_Existing_Localized_Format() _ = smart.Format("{:L(es):WeTranslateText}"); var result = smart.Format("{:L(es):WeTranslateText}"); - Assert.That(locFormatter!.LocalizedFormatCache!.Keys.Contains(result), Is.True); + Assert.That(locFormatter!.LocalizedFormatCache!.ContainsKey(result), Is.True); } [TestCase("{:L():WeTranslateText}", "Traducimos el texto", "es")] @@ -205,4 +205,4 @@ public void Combine_With_PluralLocalizationFormatter(string format, int count, s Assert.That(actual, Is.EqualTo(expected)); } } -} \ No newline at end of file +} diff --git a/src/SmartFormat.Tests/Extensions/PersistentVariableSourceTests.cs b/src/SmartFormat.Tests/Extensions/PersistentVariableSourceTests.cs index 12dd615d..e831b256 100644 --- a/src/SmartFormat.Tests/Extensions/PersistentVariableSourceTests.cs +++ b/src/SmartFormat.Tests/Extensions/PersistentVariableSourceTests.cs @@ -38,7 +38,7 @@ public void Add_And_Get_Items() Assert.That(pvs[groupName1], Is.EqualTo(vg1)); Assert.That(pvs[groupName2], Is.EqualTo(vg2)); Assert.That(pvs.Keys.Count, Is.EqualTo(2)); - Assert.That(pvs.Keys.Contains(groupName1)); + Assert.That(pvs.ContainsKey(groupName1)); Assert.That(pvs.Values.Count, Is.EqualTo(2)); Assert.That(pvs.Values.Contains(vg1)); Assert.That(pvs.Values.Contains(vg2)); diff --git a/src/SmartFormat.Tests/Extensions/VariablesGroupTests.cs b/src/SmartFormat.Tests/Extensions/VariablesGroupTests.cs index 8f13ba1b..78856e4d 100644 --- a/src/SmartFormat.Tests/Extensions/VariablesGroupTests.cs +++ b/src/SmartFormat.Tests/Extensions/VariablesGroupTests.cs @@ -42,7 +42,7 @@ public void Add_And_Get_Items() Assert.That((int) vg[var1Name].GetValue()!, Is.EqualTo(1234)); Assert.That((string) vg[var2Name].GetValue()!, Is.EqualTo("theValue")); Assert.That(vg.Keys.Count, Is.EqualTo(3)); - Assert.That(vg.Keys.Contains(var1Name)); + Assert.That(vg.ContainsKey(var1Name)); Assert.That(vg.Values.Count, Is.EqualTo(3)); Assert.That(vg.Values.Contains(var1)); Assert.That(vg.Values.Contains(var2)); diff --git a/src/SmartFormat/Extensions/ChooseFormatter.cs b/src/SmartFormat/Extensions/ChooseFormatter.cs index 1479ef30..de0e1e04 100644 --- a/src/SmartFormat/Extensions/ChooseFormatter.cs +++ b/src/SmartFormat/Extensions/ChooseFormatter.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using SmartFormat.Core.Extensions; using SmartFormat.Core.Parsing; +using SmartFormat.Core.Settings; namespace SmartFormat.Extensions { @@ -14,6 +16,8 @@ namespace SmartFormat.Extensions /// public class ChooseFormatter : IFormatter { + private CultureInfo? _cultureInfo; + /// /// Gets or sets the character used to split the option text literals. /// @@ -49,6 +53,8 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) $"Formatter named '{formattingInfo.Placeholder?.FormatterName}' requires at least 2 format options."); } + _cultureInfo = formattingInfo.FormatDetails.Provider as CultureInfo ?? CultureInfo.CurrentUICulture; + var chosenFormat = DetermineChosenFormat(formattingInfo, formats, chooseOptions); formattingInfo.FormatAsChild(chosenFormat, formattingInfo.CurrentValue); @@ -56,13 +62,10 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) return true; } - private static Format DetermineChosenFormat(IFormattingInfo formattingInfo, IList choiceFormats, + private Format DetermineChosenFormat(IFormattingInfo formattingInfo, IList choiceFormats, string[] chooseOptions) { - var currentValue = formattingInfo.CurrentValue; - var currentValueString = currentValue == null ? "null" : currentValue.ToString(); - - var chosenIndex = Array.IndexOf(chooseOptions, currentValueString); + var chosenIndex = GetChosenIndex(formattingInfo, chooseOptions, out var currentValueString); // Validate the number of formats: if (choiceFormats.Count < chooseOptions.Length) @@ -80,5 +83,49 @@ private static Format DetermineChosenFormat(IFormattingInfo formattingInfo, ILis var chosenFormat = choiceFormats[chosenIndex]; return chosenFormat; } + + private int GetChosenIndex(IFormattingInfo formattingInfo, string[] chooseOptions, out string currentValueString) + { + string valAsString; + + // null and bool types are always case-insensitive + switch (formattingInfo.CurrentValue) + { + case null: + valAsString = currentValueString = "null"; + return Array.FindIndex(chooseOptions, + t => t.Equals(valAsString, StringComparison.OrdinalIgnoreCase)); + case bool boolVal: + valAsString = currentValueString = boolVal.ToString(); + return Array.FindIndex(chooseOptions, + t => t.Equals(valAsString, StringComparison.OrdinalIgnoreCase)); + } + + valAsString = currentValueString = formattingInfo.CurrentValue.ToString(); + + return Array.FindIndex(chooseOptions, + t => AreEqual(t, valAsString, formattingInfo.FormatDetails.Settings.CaseSensitivity)); + } + + private bool AreEqual(string s1, string s2, CaseSensitivityType caseSensitivityFromSettings) + { + System.Diagnostics.Debug.Assert(_cultureInfo is not null); + var culture = _cultureInfo!; + + var toUse = caseSensitivityFromSettings == CaseSensitivity + ? caseSensitivityFromSettings + : CaseSensitivity; + + return toUse == CaseSensitivityType.CaseSensitive + ? culture.CompareInfo.Compare(s1, s2, CompareOptions.None) == 0 + : culture.CompareInfo.Compare(s1, s2, CompareOptions.IgnoreCase) == 0; + } + + /// + /// Sets or gets the for option strings. + /// Defaults to . + /// Comparison of option strings is culture-aware. + /// + public CaseSensitivityType CaseSensitivity { get; set; } = CaseSensitivityType.CaseSensitive; } }