Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend NullFormatter capabilities #199

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
afd5b77
Update issue templates
axunonb Apr 17, 2021
1d22508
Inserted a link to merged version/v3 changes
axunonb Apr 27, 2021
11c3033
Updated README
axunonb Apr 27, 2021
d1779f9
Merge remote-tracking branch 'upstream/main' into version/v3.0
axunonb Apr 29, 2021
cb5559a
Make parser fully covered with unit tests
axunonb Apr 29, 2021
e327de5
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb Apr 29, 2021
cab97df
Updated CHANGES.md
axunonb Apr 29, 2021
e8d9c93
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb Apr 29, 2021
294f810
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb Apr 29, 2021
792c391
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 3, 2021
4f45a84
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 9, 2021
e49db1e
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 9, 2021
2c562b5
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 9, 2021
2ba8c5f
Updated .editorconfig
axunonb May 9, 2021
7c3a12e
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 25, 2021
bda63e0
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 25, 2021
ef58744
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 26, 2021
67b2119
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
974224b
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
3e6e8ae
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
6219b47
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
34f2821
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
6ae6516
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 29, 2021
1d7e6f3
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 29, 2021
250270b
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 30, 2021
6550a35
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 2, 2021
a457c81
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 13, 2021
d42aaa6
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 27, 2021
1e3fccc
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 27, 2021
dc58bf2
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 27, 2021
5cdd6ae
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 29, 2021
c1f65cd
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 30, 2021
309fc5a
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 30, 2021
f7d1bbb
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 31, 2021
1062b20
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 31, 2021
6462ad3
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Aug 31, 2021
7927e19
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Sep 3, 2021
9b5bef2
Merge branch 'axuno:version/v3.0' into version/v3.0
axunonb Sep 5, 2021
c7484a4
NullFormatter processes complex formats
axunonb Sep 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/SmartFormat.Tests/Extensions/ChooseFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using FluentAssertions.Formatting;
using NUnit.Framework;
using SmartFormat.Core.Formatting;
Expand Down Expand Up @@ -101,5 +102,27 @@ public void Choose_throws_when_choices_are_too_few_or_too_many(string format, ob
Assert.Throws<FormattingException>(() => smart.Format(format, arg0));
}

[TestCase(1234, 9999, "1,234.00")]
[TestCase(null, 9999, "9,999.00")]
public void May_Contain_Nested_Formats_As_Choice(int? nullableInt, int valueIfNull, string expected)
{
var data = new { NullableInt = nullableInt, IntValueIfNull = valueIfNull};
var smart = Smart.CreateDefaultSmartFormat();
var result = smart.Format(CultureInfo.InvariantCulture, "{NullableInt:choose(null):{IntValueIfNull:N2}|{:N2}}", data);

Assert.That(result, Is.EqualTo(expected));
}

[TestCase(1234, 9999, "1,234.00")] // first choose formatter
[TestCase(null, 1000, "1k")] // first + nested choose formatter, option 1
[TestCase(null, 2000, "2k")] // first + nested choose formatter, option 2
public void May_Contain_Nested_Choose_Formats(int? nullableInt, int valueIfNull, string expected)
{
var data = new { NullableInt = nullableInt, IntValueIfNull = valueIfNull};
var smart = Smart.CreateDefaultSmartFormat();
var result = smart.Format(CultureInfo.InvariantCulture, "{NullableInt:choose(null):{IntValueIfNull:choose(1000|2000):1k|2k}|{:N2}}", data);

Assert.That(result, Is.EqualTo(expected));
}
}
}
67 changes: 45 additions & 22 deletions src/SmartFormat.Tests/Extensions/NullFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using SmartFormat.Core.Extensions;
using SmartFormat.Core.Formatting;
using SmartFormat.Core.Settings;
using SmartFormat.Extensions;

Expand Down Expand Up @@ -35,6 +37,14 @@ public void Null_Without_Format_Should_Be_Empty_String_CompatibilityMode()
Assert.That(smart.Format("{JustNull}", new { JustNull = default(object)} ), Is.EqualTo(string.Empty));
}

[TestCase("")] // empty format
[TestCase("something")]
public void NotNull_Should_Be_Empty_String(string format)
{
var smart = GetFormatter();
Assert.That(smart.Format($"{{NotNull:{format}}}", new { NotNull = ""} ), Is.EqualTo(string.Empty));
}

[TestCase("")] // empty format
[TestCase("nothing")]
public void Null_With_Format_Should_Be_Format_String(string format)
Expand All @@ -43,38 +53,51 @@ public void Null_With_Format_Should_Be_Format_String(string format)
Assert.That(smart.Format($"{{JustNull:isnull:{format}}}", new { JustNull = default(object)} ), Is.EqualTo(format));
}

[TestCase("")] // empty format
[TestCase("something")]
public void NotNull_Should_Be_Empty_String(string format)
[TestCase(null, "")]
[TestCase("a string", "")]
[TestCase(new long[] {5, 6, 7}, "")]
public void Empty_FormatOption_Should_Only_Be_Output_If_Argument_Is_Null(object? value, string expected)
{
var smart = GetFormatter();
Assert.That(smart.Format($"{{NotNull:{format}}}", new { NotNull = ""} ), Is.EqualTo(string.Empty));
Assert.That(smart.Format("{TheValue:isnull:}", new {TheValue = value}), Is.EqualTo(expected));
}

[TestCase(null)]
[TestCase("a string")]
[TestCase(new long[] {5, 6, 7})]
public void Format_Should_Only_Be_Output_If_Argument_Is_Null(object? value)
[TestCase(null, "It's null", "It's null")] // Null value and format for null
[TestCase("a string", "It's null|It's a string", "It's a string")] // With format for "not null"
[TestCase("a string", "It's null", "")] // No format for "not null"
[TestCase(new long[] {5, 6, 7}, "It's null|Numbers", "Numbers")] // With format for "not null"
public void FormatOption_Should_Only_Be_Output_If_Argument_Is_Null(object? value, string formats, string expected)
{
var smart = GetFormatter();
Assert.That(smart.Format("{TheValue:isnull:}", new {TheValue = value}), Is.EqualTo(string.Empty));
Assert.That(smart.Format("{TheValue:isnull:nothing}", new {TheValue = value}),
value == null ? Is.EqualTo("nothing") : Is.EqualTo(string.Empty));
Assert.That(smart.Format("{TheValue:isnull:" + formats + "}", new {TheValue = value}), Is.EqualTo(expected));
Assert.That(smart.Format("{0:isnull:" + formats + "}", value), Is.EqualTo(expected));
}

[TestCase(null, "Argument is null")]
[TestCase("Argument has this value", "Argument has this value")]
public void Combine_NullFormatter_With_Other_Formatter(object value, string expected)
[TestCase(null, 0, "Was null")] // first choose formatter
[TestCase(123, 1000, "1k")] // first + nested choose formatter, option 1
[TestCase(123, 2000, "2,000.00")] // first + nested choose formatter, option 2
public void May_Contain_Nested_Formats(int? nullableInt, int valueIfNotNull, string expected)
{
var smart = new SmartFormatter();
smart.AddExtensions(new DefaultSource());
smart.AddExtensions(new NullFormatter(), new DefaultFormatter());

// NullFormatter will output only, if value is null
// DefaultFormatter will render null as string.Empty
var result = smart.Format($"{{0:isnull:{expected}}}{{0}}", value!);

var smart = GetFormatter();
smart.AddExtensions(new ChooseFormatter());
var data = new { NullableInt = nullableInt, IntValueIfNotNull = valueIfNotNull};
var result = smart.Format(CultureInfo.InvariantCulture, "{NullableInt:isnull:Was null|{IntValueIfNotNull:choose(1000|2000):1k|{:N2}}}", data);

Assert.That(result, Is.EqualTo(expected));
}

[Test]
public void NullFormatter_Must_Not_Contain_Choose_Options()
{
var smart = GetFormatter();
Assert.That(() => smart.Format("{0:isnull(op|ti|ons):Is null}", 123), Throws.InstanceOf<FormattingException>());
}

[Test]
public void NullFormatter_Format_Count_Must_Be_1_or_2()
{
var smart = GetFormatter();
Assert.That(() => smart.Format("{0:isnull:1|2|3}", 123), Throws.InstanceOf<FormattingException>(), "No format included");
}
}
}
64 changes: 54 additions & 10 deletions src/SmartFormat/Extensions/NullFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
using System.Collections.Generic;
using System.Text;
using SmartFormat.Core.Extensions;
using SmartFormat.Core.Parsing;

namespace SmartFormat.Extensions
{
/// <summary>
/// The class formats <see langword="null"/> values.
/// </summary>
/// <example>
/// Smart.Format("{0:isnull:It's null}", arg)
/// Smart.Format("{0:isnull:It's null|Not null}", arg)
/// Smart.Format("{0:isnull:It's null|{}}", arg)
/// </example>
public class NullFormatter : IFormatter
{
/// <summary>
Expand All @@ -27,21 +33,59 @@ public class NullFormatter : IFormatter
///<inheritdoc/>
public bool CanAutoDetect { get; set; } = false;

/// <inheritdoc />
/// <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 format = formattingInfo.Format;
var current = formattingInfo.CurrentValue;
var currentValue = formattingInfo.CurrentValue;
var chooseOptions = formattingInfo.FormatterOptions.AsSpan().Trim();
var formats = formattingInfo.Format?.Split(SplitChar);

// Check whether arguments can be handled by this formatter
if (chooseOptions.Length != 0)
{
// Auto detection calls just return a failure to evaluate
if (string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName))
return false;

// throw, if the formatter has been called explicitly
throw new FormatException(
$"Formatter named '{formattingInfo.Placeholder?.FormatterName}' does not allow choose options");
}

if (formats is null || formats.Count is < 1 or > 2)
{
// Auto detection calls just return a failure to evaluate
if (string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName))
return false;

switch (current)
// throw, if the formatter has been called explicitly
throw new FormatException(
$"Formatter named '{formattingInfo.Placeholder?.FormatterName}' must have 1 or 2 format options");
}

// Use the format for null
if (currentValue is null)
{
case null when format is not null:
formattingInfo.Write(format.GetLiteralText());
return true;
default:
formattingInfo.Write(string.Empty);
return true;
formattingInfo.FormatAsChild(formats[0], currentValue);
return true;
}

// Use the format for a value other than null
if (formats.Count > 1)
{
formattingInfo.FormatAsChild(formats[1], currentValue);
return true;
}

// There is no format for a value other than null
formattingInfo.Write(ReadOnlySpan<char>.Empty);

return true;
}
}
}