diff --git a/CHANGES.md b/CHANGES.md index b7cb18d5..363388b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -309,6 +309,7 @@ SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleS a) Get the culture from the `FormattingInfo.FormatterOptions`.
b) Get the culture from the `IFormatProvider` argument (which may be a `CultureInfo`) to `SmartFormatter.Format(IFormatProvider, string, object?[])`
c) The `CultureInfo.CurrentUICulture`
+ d) `CultureInfo.InvariantCulture` maps to `CultureInfo.GetCultureInfo("en")` ([#243](https://github.com/axuno/SmartFormat/pull/243)) ### 20. Refactored `TimeFormatter` ([#220](https://github.com/axuno/SmartFormat/pull/220), [#221](https://github.com/axuno/SmartFormat/pull/221), [#234](https://github.com/axuno/SmartFormat/pull/234)) @@ -502,9 +503,24 @@ var formatter = new SmartFormatter() ``` ### 24. Miscellaneous - * Since [#228](https://github.com/axuno/SmartFormat/pull/228) there are no more `Cysharp.Text` classes used in the `SmartFormat` namespace - * Created class `ZStringBuilder` as a wrapper around `Utf16ValueStringBuilder`. - * Replaced occurrences of `Utf16ValueStringBuilder` with `ZStringBuilder`. + +a) Cyhsarp.Text + +Since [#228](https://github.com/axuno/SmartFormat/pull/228) there are no more `Cysharp.Text` classes used in the `SmartFormat` namespace + * Created class `ZStringBuilder` as a wrapper around `Utf16ValueStringBuilder`. + * Replaced occurrences of `Utf16ValueStringBuilder` with `ZStringBuilder`. + +b) Split character for options and formats + +Since [#243](https://github.com/axuno/SmartFormat/pull/243) the character to split options and formats can be changed. This allows having the default split character `|` as part of the output string. +Affects `ChooseFormatter`, `ConditionalFormatter`, `IsMatchFormatter`, `ListFormatter`, `PluralLocalizationFormatter`, `SubStringFormatter`. Example: +```Csharp +var smart = Smart.CreateDefaultSmartFormat(); +// Change SplitChar from | to TAB, so we can use | for the output string +smart.GetFormatterExtension()!.SplitChar = '\t'; +_ = smart.Format({0:cond:|No|\t|Yes|}", 1); +// Result: "|Yes|" +``` v2.7.2 === diff --git a/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs b/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs index dfa1e7bd..0ab04d16 100644 --- a/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using SmartFormat.Core.Formatting; using SmartFormat.Core.Settings; +using SmartFormat.Extensions; using SmartFormat.Tests.TestUtils; namespace SmartFormat.Tests.Extensions @@ -33,6 +34,16 @@ public void Choose_should_work_with_numbers_strings_and_booleans(string format, Assert.AreEqual(expectedResult, smart.Format(format, arg0)); } + [Test] + public void Choose_With_Changed_SplitChar() + { + var smart = Smart.CreateDefaultSmartFormat(); + // Set SplitChar from | to TAB, so we can use | for the output string + smart.GetFormatterExtension()!.SplitChar = '\t'; + var result = smart.Format("{0:choose(1\t2\t3):|one|\t|two|\t|three|}", 2); + 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")] @@ -123,4 +134,4 @@ public void May_Contain_Nested_Choose_Formats(int? nullableInt, int valueIfNull, Assert.That(result, Is.EqualTo(expected)); } } -} \ No newline at end of file +} diff --git a/src/SmartFormat.Tests/Extensions/ConditionalFormatterTests.cs b/src/SmartFormat.Tests/Extensions/ConditionalFormatterTests.cs index ccc9211f..36926019 100644 --- a/src/SmartFormat.Tests/Extensions/ConditionalFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/ConditionalFormatterTests.cs @@ -1,6 +1,7 @@ using System; using NUnit.Framework; using SmartFormat.Core.Formatting; +using SmartFormat.Extensions; using SmartFormat.Tests.TestUtils; using SmartFormat.Utilities; @@ -38,6 +39,17 @@ public void Test_Bool(string format, string expected) smart.Test(format, args, expected); } + [TestCase("{0:cond:|Yes|\t|No|}", 0, "|Yes|")] + [TestCase("{0:cond:|Yes|\t|No|}", 1, "|No|")] + public void Test_With_Changed_SplitChar(string format, int arg, string expected) + { + var smart = Smart.CreateDefaultSmartFormat(); + // Set SplitChar from | to TAB, so we can use | for the output string + smart.GetFormatterExtension()!.SplitChar = '\t'; + var result = smart.Format(format, arg); + Assert.That(result, Is.EqualTo(expected)); + } + [Test] public void Explicit_Formatter_With_Not_Enough_Parameters_Should_Throw() { diff --git a/src/SmartFormat.Tests/Extensions/IsMatchFormatterTests.cs b/src/SmartFormat.Tests/Extensions/IsMatchFormatterTests.cs index 4498833e..6d2407a9 100644 --- a/src/SmartFormat.Tests/Extensions/IsMatchFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/IsMatchFormatterTests.cs @@ -44,8 +44,19 @@ public void Less_Than_2_Format_Options_Should_Throw() // less than 2 format options should throw exception Assert.Throws(() => _formatter.Format("{theKey:ismatch(^.+123.+$):Dummy content}", _variable)); - Assert.DoesNotThrow(() => - _formatter.Format("{theKey:ismatch(^.+123.+$):Dummy content|2nd option}", _variable)); + } + + // The "{}" in the format will (as always) output the matching variable + [TestCase("{theKey:ismatch(^.+123.+$):|Has match for '{}'|\t|No match|}", "|Has match for 'Some123Content'|")] + [TestCase("{theKey:ismatch(^.+999.+$):|Has match for '{}'|\t|No match|}", "|No match|")] + public void Test_With_Changed_SplitChar(string format, string expected) + { + var variable = new Dictionary { {"theKey", "Some123Content"}}; + var smart = Smart.CreateDefaultSmartFormat(); + // Set SplitChar from | to TAB, so we can use | for the output string + smart.GetFormatterExtension()!.SplitChar = '\t'; + var result = smart.Format(format, variable); + Assert.That(result, Is.EqualTo(expected)); } [Test] diff --git a/src/SmartFormat.Tests/Extensions/ListFormatterTests.cs b/src/SmartFormat.Tests/Extensions/ListFormatterTests.cs index 8d07eecd..b830a431 100644 --- a/src/SmartFormat.Tests/Extensions/ListFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/ListFormatterTests.cs @@ -38,6 +38,17 @@ public void Simple_List() Assert.AreEqual("one, two, and three", result); } + [Test] + public void Simple_List_Changed_SplitChar() + { + var smart = Smart.CreateDefaultSmartFormat(); + // Set SplitChar from | to TAB, so we can use | for the output string + smart.GetFormatterExtension()!.SplitChar = '\t'; + var items = new[] { "one", "two", "three" }; + var result = smart.Format("{0:list:{}\t|\t|}", new object[] { items }); // important: not only "items" as the parameter + Assert.AreEqual("one|two|three", result); + } + [Test] public void Empty_List() { diff --git a/src/SmartFormat.Tests/Extensions/PluralLocalizationFormatterTests.cs b/src/SmartFormat.Tests/Extensions/PluralLocalizationFormatterTests.cs index 69d2597e..7a6e187c 100644 --- a/src/SmartFormat.Tests/Extensions/PluralLocalizationFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/PluralLocalizationFormatterTests.cs @@ -278,5 +278,23 @@ public void Nested_PlaceHolders_Pluralization(int numOfPeople, string format, bo Assert.That(result, numOfPeople == 1 ? Is.EqualTo("There is a person.") : Is.EqualTo("There are 2 people.")); } + + [TestCase(1, "There {People.Count:plural:|is a person|.\t|are {} people|.}", "There |is a person|.")] + [TestCase(2, "There {People.Count:plural:|is a person|.\t|are {} people|.}", "There |are 2 people|.")] + public void Pluralization_With_Changed_SplitChar(int numOfPeople, string format, string expected) + { + var data = numOfPeople == 1 + ? new {People = new List {new {Name = "Name 1", Age = 20}}} + : new {People = new List {new {Name = "Name 1", Age = 20}, new {Name = "Name 2", Age = 30}}}; + + var smart = new SmartFormatter() + .AddExtensions(new ReflectionSource()) + // Set SplitChar from | to TAB, so we can use | for the output string + .AddExtensions(new PluralLocalizationFormatter { SplitChar = '\t'}, + new DefaultFormatter()); + + var result = smart.Format(format, data); + Assert.That(result, Is.EqualTo(expected)); + } } } diff --git a/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs b/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs index 728f5f93..22efd721 100644 --- a/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs @@ -138,8 +138,18 @@ public void DataItemIsNull() var smart = GetFormatter(); var ssf = smart.GetFormatterExtension(); ssf!.NullDisplayString = "???"; - ssf!.ParameterDelimiter = '*'; - Assert.AreEqual(ssf.NullDisplayString, smart.Format("{Name:substr(0*3)}", new Dictionary { { "Name", null } })); + Assert.AreEqual(ssf.NullDisplayString, smart.Format("{Name:substr(0,3)}", new Dictionary { { "Name", null } })); + } + + [Test] + public void Test_With_Changed_SplitChar() + { + var smart = GetFormatter(); + var currentSplitChar = smart.GetFormatterExtension()!.SplitChar; + // Change SplitChar from default ',' to '|' + smart.GetFormatterExtension()!.SplitChar = '|'; + Assert.AreEqual("Joh", smart.Format("{Name:substr(-4|-1)}", _people.First())); + Assert.That(currentSplitChar, Is.EqualTo(',')); // make sure there was a change } [Test] @@ -165,4 +175,4 @@ public void ImplicitFormatterEvaluation_With_Wrong_Args_Should_Fail() FormattingInfoExtensions.Create("{0::(0,2)}", new List(new[] {new object()}))), Is.EqualTo(false)); } } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Core/Formatting/IFormatter.cs b/src/SmartFormat/Core/Formatting/IFormatter.cs index a41bc8e6..be9a5d0b 100644 --- a/src/SmartFormat/Core/Formatting/IFormatter.cs +++ b/src/SmartFormat/Core/Formatting/IFormatter.cs @@ -22,7 +22,7 @@ public interface IFormatter /// (when no formatter name is specified in the input format string). /// For example, "{0:N2}" will implicitly call extensions marked as . /// Implicit formatter invocations should not throw exceptions. - /// With =, the formatter can only be + /// With == , the formatter can only be /// called by its name in the input format string. /// /// @@ -36,4 +36,4 @@ public interface IFormatter /// bool TryEvaluateFormat(IFormattingInfo formattingInfo); } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Core/Parsing/Format.cs b/src/SmartFormat/Core/Parsing/Format.cs index 53bc5434..4cc57198 100644 --- a/src/SmartFormat/Core/Parsing/Format.cs +++ b/src/SmartFormat/Core/Parsing/Format.cs @@ -296,13 +296,12 @@ public IList Split(char search) return _splitCache; } - /// /// Splits the items by the given search character. /// /// e search character used to split. /// The maximum number of of type . - /// + /// An of s. public IList Split(char search, int maxCount) { var splits = FindAll(search, maxCount); diff --git a/src/SmartFormat/Extensions/ChooseFormatter.cs b/src/SmartFormat/Extensions/ChooseFormatter.cs index bd292f4b..519d6575 100644 --- a/src/SmartFormat/Extensions/ChooseFormatter.cs +++ b/src/SmartFormat/Extensions/ChooseFormatter.cs @@ -82,4 +82,4 @@ private static Format DetermineChosenFormat(IFormattingInfo formattingInfo, ILis return chosenFormat; } } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Extensions/ConditionalFormatter.cs b/src/SmartFormat/Extensions/ConditionalFormatter.cs index 210de795..391a77f6 100644 --- a/src/SmartFormat/Extensions/ConditionalFormatter.cs +++ b/src/SmartFormat/Extensions/ConditionalFormatter.cs @@ -34,6 +34,11 @@ private static readonly Regex _complexConditionPattern /// public bool CanAutoDetect { get; set; } = true; + + /// + /// Gets or sets the character used to split the option text literals. + /// + public char SplitChar { get; set; } = '|'; /// public bool TryEvaluateFormat(IFormattingInfo formattingInfo) @@ -45,7 +50,7 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) if (format?.BaseString.Length > 0 && format.BaseString[format.StartIndex] == ':') format = format.Substring(1); // See if the format string contains un-nested "|": - var parameters = format is not null ? format.Split('|') : new List(0); + var parameters = format is not null ? format.Split(SplitChar) : new List(0); // Check whether arguments can be handled by this formatter if (format is null || parameters.Count == 1) @@ -234,4 +239,4 @@ private static bool TryEvaluateCondition(Format parameter, decimal value, out bo return true; } } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Extensions/IsMatchFormatter.cs b/src/SmartFormat/Extensions/IsMatchFormatter.cs index 6fb08eb5..2678b4ec 100644 --- a/src/SmartFormat/Extensions/IsMatchFormatter.cs +++ b/src/SmartFormat/Extensions/IsMatchFormatter.cs @@ -33,11 +33,16 @@ public class IsMatchFormatter : IFormatter /// public bool CanAutoDetect { get; set; } = false; + /// + /// Gets or sets the character used to split the option text literals. + /// + public char SplitChar { get; set; } = '|'; + /// public bool TryEvaluateFormat(IFormattingInfo formattingInfo) { var expression = formattingInfo.FormatterOptions; - var formats = formattingInfo.Format?.Split('|'); + var formats = formattingInfo.Format?.Split(SplitChar); // Check whether arguments can be handled by this formatter if (formats is null || formats.Count != 2) @@ -69,4 +74,4 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) /// public RegexOptions RegexOptions { get; set; } } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Extensions/ListFormatter.cs b/src/SmartFormat/Extensions/ListFormatter.cs index 8b32e692..2d4ac04b 100644 --- a/src/SmartFormat/Extensions/ListFormatter.cs +++ b/src/SmartFormat/Extensions/ListFormatter.cs @@ -57,6 +57,11 @@ public class ListFormatter : IFormatter, ISource, IInitializer /// public bool CanAutoDetect { get; set; } = true; + /// + /// Gets or sets the character used to split the option text literals. + /// + public char SplitChar { get; set; } = '|'; + /// /// This allows an integer to be used as a selector to index an array (or list). /// This is better described using an example: @@ -155,7 +160,7 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) // See if the format string contains un-nested "|": using var splitListPooledObject = SplitListPool.Instance.Get(out var splitList); - var parameters = (SplitList) (format is not null ? format.Split('|', 4) : splitList); + var parameters = (SplitList) (format is not null ? format.Split(SplitChar, 4) : splitList); // Check whether arguments can be handled by this formatter if (format is null || parameters.Count < 2 || current is not IEnumerable currentAsEnumerable @@ -186,7 +191,7 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) if (!itemFormat.HasNested) { // The format is not nested, - // so we will treat it as an itemFormat: + // so we will treat it as an ItemFormat: var newItemFormat = FormatPool.Instance.Get().Initialize(_smartSettings, itemFormat.BaseString, itemFormat.StartIndex, itemFormat.EndIndex, true); @@ -283,4 +288,4 @@ public void Initialize(SmartFormatter smartFormatter) _isInitialized = true; } } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Extensions/PluralLocalizationFormatter.cs b/src/SmartFormat/Extensions/PluralLocalizationFormatter.cs index 1ec02fdf..a373a32d 100644 --- a/src/SmartFormat/Extensions/PluralLocalizationFormatter.cs +++ b/src/SmartFormat/Extensions/PluralLocalizationFormatter.cs @@ -72,6 +72,11 @@ public PluralLocalizationFormatter(string defaultTwoLetterIsoLanguageName) /// public bool CanAutoDetect { get; set; } = true; + /// + /// Gets or sets the character used to split the option text literals. + /// + public char SplitChar { get; set; } = '|'; + /// public bool TryEvaluateFormat(IFormattingInfo formattingInfo) { @@ -82,7 +87,7 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) if (format == null || format.BaseString.Length > 0 && format.BaseString[format.StartIndex] == ':') return false; // Extract the plural words from the format string: - var pluralWords = format.Split('|'); + var pluralWords = format.Split(SplitChar); // This extension requires at least two plural words: if (pluralWords.Count == 1) { @@ -148,6 +153,7 @@ private static PluralRules.PluralRuleDelegate GetPluralRule(IFormattingInfo form if (pluralRuleProvider != null) return pluralRuleProvider.GetPluralRule(); // No CustomPluralRuleProvider, so use the CultureInfo + return PluralRules.GetPluralRule(culture.TwoLetterISOLanguageName); } @@ -160,7 +166,12 @@ private static CultureInfo GetCultureInfo(IFormattingInfo formattingInfo) if (formattingInfo.FormatDetails.Provider is CultureInfo ci) cultureInfo = ci; else - cultureInfo = CultureInfo.CurrentUICulture; // also used this way by ResourceManager + cultureInfo = CultureInfo.CurrentUICulture; + + // There is no pluralization rule for invariant culture (TwoLetterISOLanguageName == "iv"), + // so we take English as default + if(cultureInfo.Equals(CultureInfo.InvariantCulture)) + cultureInfo = CultureInfo.GetCultureInfo("en"); } else { @@ -213,4 +224,4 @@ public PluralRules.PluralRuleDelegate GetPluralRule() return _pluralRule; } } -} \ No newline at end of file +} diff --git a/src/SmartFormat/Extensions/SubStringFormatter.cs b/src/SmartFormat/Extensions/SubStringFormatter.cs index 94f6a697..0ed3df93 100644 --- a/src/SmartFormat/Extensions/SubStringFormatter.cs +++ b/src/SmartFormat/Extensions/SubStringFormatter.cs @@ -26,9 +26,9 @@ public class SubStringFormatter : IFormatter public bool CanAutoDetect { get; set; } = false; /// - /// The delimiter to separate parameters, defaults to comma. + /// Gets or sets the character used to split the option text literals. /// - public char ParameterDelimiter { get; set; } = ','; + public char SplitChar { get; set; } = ','; /// /// Get or set the string to display for NULL values, defaults to . @@ -43,7 +43,7 @@ public class SubStringFormatter : IFormatter /// public bool TryEvaluateFormat(IFormattingInfo formattingInfo) { - var parameters = formattingInfo.FormatterOptions.Split(ParameterDelimiter) ?? Array.Empty(); + var parameters = formattingInfo.FormatterOptions.Split(SplitChar); if (formattingInfo.CurrentValue is not (string or null) || parameters.Length == 1 && parameters[0].Length == 0) { // Auto detection calls just return a failure to evaluate @@ -61,7 +61,7 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) formattingInfo.Write(NullDisplayString); return true; } - + var (startPos, length) = GetStartAndLength(currentValue, parameters); switch(OutOfRangeBehavior)