Skip to content

Commit

Permalink
Char to split options and formats can be changed (#243)
Browse files Browse the repository at this point in the history
* SplitChar for formatters can now be changed from default '|' to another char (like ' \t')

* Affects ChooseFormatter, ConditionalFormatter, IsMatchFormatter, ListFormatter, PluralLocalizationFormatter, SubStringFormatter
* With "{}" in the format part, the matching variable will show

* Invariant culture maps to English. Reasoning: There is no plural rule for invariant culture ("iv")
  • Loading branch information
axunonb authored Feb 10, 2022
1 parent b134f38 commit 2a43b58
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 28 deletions.
22 changes: 19 additions & 3 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.<br/>
b) Get the culture from the `IFormatProvider` argument (which may be a `CultureInfo`) to `SmartFormatter.Format(IFormatProvider, string, object?[])`<br/>
c) The `CultureInfo.CurrentUICulture`<br/>
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))
Expand Down Expand Up @@ -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<ConditionalFormatter>()!.SplitChar = '\t';
_ = smart.Format({0:cond:|No|\t|Yes|}", 1);
// Result: "|Yes|"
```

v2.7.2
===
Expand Down
13 changes: 12 additions & 1 deletion src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ChooseFormatter>()!.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")]
Expand Down Expand Up @@ -123,4 +134,4 @@ public void May_Contain_Nested_Choose_Formats(int? nullableInt, int valueIfNull,
Assert.That(result, Is.EqualTo(expected));
}
}
}
}
12 changes: 12 additions & 0 deletions src/SmartFormat.Tests/Extensions/ConditionalFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using NUnit.Framework;
using SmartFormat.Core.Formatting;
using SmartFormat.Extensions;
using SmartFormat.Tests.TestUtils;
using SmartFormat.Utilities;

Expand Down Expand Up @@ -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<ConditionalFormatter>()!.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()
{
Expand Down
15 changes: 13 additions & 2 deletions src/SmartFormat.Tests/Extensions/IsMatchFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,19 @@ public void Less_Than_2_Format_Options_Should_Throw()
// less than 2 format options should throw exception
Assert.Throws<FormattingException>(() =>
_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<string, object> { {"theKey", "Some123Content"}};
var smart = Smart.CreateDefaultSmartFormat();
// Set SplitChar from | to TAB, so we can use | for the output string
smart.GetFormatterExtension<IsMatchFormatter>()!.SplitChar = '\t';
var result = smart.Format(format, variable);
Assert.That(result, Is.EqualTo(expected));
}

[Test]
Expand Down
11 changes: 11 additions & 0 deletions src/SmartFormat.Tests/Extensions/ListFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ListFormatter>()!.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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> {new {Name = "Name 1", Age = 20}}}
: new {People = new List<object> {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));
}
}
}
16 changes: 13 additions & 3 deletions src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,18 @@ public void DataItemIsNull()
var smart = GetFormatter();
var ssf = smart.GetFormatterExtension<SubStringFormatter>();
ssf!.NullDisplayString = "???";
ssf!.ParameterDelimiter = '*';
Assert.AreEqual(ssf.NullDisplayString, smart.Format("{Name:substr(0*3)}", new Dictionary<string, string?> { { "Name", null } }));
Assert.AreEqual(ssf.NullDisplayString, smart.Format("{Name:substr(0,3)}", new Dictionary<string, string?> { { "Name", null } }));
}

[Test]
public void Test_With_Changed_SplitChar()
{
var smart = GetFormatter();
var currentSplitChar = smart.GetFormatterExtension<SubStringFormatter>()!.SplitChar;
// Change SplitChar from default ',' to '|'
smart.GetFormatterExtension<SubStringFormatter>()!.SplitChar = '|';
Assert.AreEqual("Joh", smart.Format("{Name:substr(-4|-1)}", _people.First()));
Assert.That(currentSplitChar, Is.EqualTo(',')); // make sure there was a change
}

[Test]
Expand All @@ -165,4 +175,4 @@ public void ImplicitFormatterEvaluation_With_Wrong_Args_Should_Fail()
FormattingInfoExtensions.Create("{0::(0,2)}", new List<object?>(new[] {new object()}))), Is.EqualTo(false));
}
}
}
}
4 changes: 2 additions & 2 deletions src/SmartFormat/Core/Formatting/IFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="CanAutoDetect"/>.
/// Implicit formatter invocations should not throw exceptions.
/// With <see cref="CanAutoDetect"/>=<see langword="false"/>, the formatter can only be
/// With <see cref="CanAutoDetect"/> == <see langword="false"/>, the formatter can only be
/// called by its name in the input format string.
/// </summary>
/// <remarks>
Expand All @@ -36,4 +36,4 @@ public interface IFormatter
/// </summary>
bool TryEvaluateFormat(IFormattingInfo formattingInfo);
}
}
}
3 changes: 1 addition & 2 deletions src/SmartFormat/Core/Parsing/Format.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,12 @@ public IList<Format> Split(char search)

return _splitCache;
}

/// <summary>
/// Splits the <see cref="Format"/> items by the given search character.
/// </summary>
/// <param name="search">e search character used to split.</param>
/// <param name="maxCount">The maximum number of <see cref="IList"/> of type <see cref="Format"/>.</param>
/// <returns></returns>
/// <returns>An <see cref="IList{T}"/> of <see cref="Format"/>s.</returns>
public IList<Format> Split(char search, int maxCount)
{
var splits = FindAll(search, maxCount);
Expand Down
2 changes: 1 addition & 1 deletion src/SmartFormat/Extensions/ChooseFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ private static Format DetermineChosenFormat(IFormattingInfo formattingInfo, ILis
return chosenFormat;
}
}
}
}
9 changes: 7 additions & 2 deletions src/SmartFormat/Extensions/ConditionalFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ private static readonly Regex _complexConditionPattern

///<inheritdoc/>
public bool CanAutoDetect { get; set; } = true;

/// <summary>
/// Gets or sets the character used to split the option text literals.
/// </summary>
public char SplitChar { get; set; } = '|';

///<inheritdoc />
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
Expand All @@ -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<Format>(0);
var parameters = format is not null ? format.Split(SplitChar) : new List<Format>(0);

// Check whether arguments can be handled by this formatter
if (format is null || parameters.Count == 1)
Expand Down Expand Up @@ -234,4 +239,4 @@ private static bool TryEvaluateCondition(Format parameter, decimal value, out bo
return true;
}
}
}
}
9 changes: 7 additions & 2 deletions src/SmartFormat/Extensions/IsMatchFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ public class IsMatchFormatter : IFormatter
///<inheritdoc/>
public bool CanAutoDetect { get; set; } = false;

/// <summary>
/// Gets or sets the character used to split the option text literals.
/// </summary>
public char SplitChar { get; set; } = '|';

///<inheritdoc />
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)
Expand Down Expand Up @@ -69,4 +74,4 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
/// </summary>
public RegexOptions RegexOptions { get; set; }
}
}
}
11 changes: 8 additions & 3 deletions src/SmartFormat/Extensions/ListFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public class ListFormatter : IFormatter, ISource, IInitializer
///<inheritdoc/>
public bool CanAutoDetect { get; set; } = true;

/// <summary>
/// Gets or sets the character used to split the option text literals.
/// </summary>
public char SplitChar { get; set; } = '|';

/// <summary>
/// This allows an integer to be used as a selector to index an array (or list).
/// This is better described using an example:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -283,4 +288,4 @@ public void Initialize(SmartFormatter smartFormatter)
_isInitialized = true;
}
}
}
}
17 changes: 14 additions & 3 deletions src/SmartFormat/Extensions/PluralLocalizationFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public PluralLocalizationFormatter(string defaultTwoLetterIsoLanguageName)
///<inheritdoc/>
public bool CanAutoDetect { get; set; } = true;

/// <summary>
/// Gets or sets the character used to split the option text literals.
/// </summary>
public char SplitChar { get; set; } = '|';

///<inheritdoc />
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
}

Expand All @@ -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
{
Expand Down Expand Up @@ -213,4 +224,4 @@ public PluralRules.PluralRuleDelegate GetPluralRule()
return _pluralRule;
}
}
}
}
8 changes: 4 additions & 4 deletions src/SmartFormat/Extensions/SubStringFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public class SubStringFormatter : IFormatter
public bool CanAutoDetect { get; set; } = false;

/// <summary>
/// The delimiter to separate parameters, defaults to comma.
/// Gets or sets the character used to split the option text literals.
/// </summary>
public char ParameterDelimiter { get; set; } = ',';
public char SplitChar { get; set; } = ',';

/// <summary>
/// Get or set the string to display for NULL values, defaults to <see cref="string.Empty"/>.
Expand All @@ -43,7 +43,7 @@ public class SubStringFormatter : IFormatter
///<inheritdoc />
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
{
var parameters = formattingInfo.FormatterOptions.Split(ParameterDelimiter) ?? Array.Empty<string>();
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
Expand All @@ -61,7 +61,7 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
formattingInfo.Write(NullDisplayString);
return true;
}

var (startPos, length) = GetStartAndLength(currentValue, parameters);

switch(OutOfRangeBehavior)
Expand Down

0 comments on commit 2a43b58

Please sign in to comment.