Skip to content

Commit

Permalink
Alignment option always available (#174)
Browse files Browse the repository at this point in the history
**Refactored implementation for string alignment** 
Example: `Smart.Format("{placeholder,15}")` where ",15" is the string alignment

* Modified `ListFormatter` so that items can be aligned, but the spacers stay untouched
* `IFormattingInfo.Alignment` now returns the alignment of the current `Placeholder`, or - if this is null - the `Alignment` of any parent `IFormattingInfo` that is not zero.
* Aligment is now implemented into class `FormattingInfo`, so it is always available. Former implementation in `DefaultFormatter` is removed.
* Introduced `FormatterSettings.AlignmentFillCharacter`, to the the fill character can be customized. Default is space (0x20), like with `string.Format`.
* Renamed `IFormattingInfo.Write(Format format, object value)` to `FormatAsChild(Format format, object value)` to make clear that nothing is written to `IOutput` (this happens in a next step).
* Added dedicated AlignmentTests
  • Loading branch information
axunonb committed Jun 4, 2021
1 parent 31735db commit d6e2a26
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 160 deletions.
123 changes: 123 additions & 0 deletions src/SmartFormat.Tests/Core/AlignmentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using SmartFormat.Extensions;
using SmartFormat.Utilities;

namespace SmartFormat.Tests.Core
{
[TestFixture]
public class AlignmentTests
{
private SmartFormatter GetSimpleFormatter()
{
var formatter = new SmartFormatter();
formatter.FormatterExtensions.Add(new DefaultFormatter());
formatter.SourceExtensions.Add(new ReflectionSource(formatter));
formatter.SourceExtensions.Add(new DefaultSource(formatter));
return formatter;
}

[Test]
public void Formatter_PreAlign()
{
const string name = "Joe";
var obj = new { name = name };
var result = GetSimpleFormatter().Format("Name: {name,10}| Column 2", obj);
Assert.That(result, Is.EqualTo("Name: Joe| Column 2"));
}

[Test]
public void Formatter_PostAlign()
{
const string name = "Joe";
var obj = new { name = name };
var result = GetSimpleFormatter().Format("Name: {name,-10}| Column 2", obj);
Assert.That(result, Is.EqualTo("Name: Joe | Column 2"));
}

[Test]
public void Formatter_PostAlign_Custom_AlignmentChar()
{
const string name = "Joe";
var obj = new { name = name };
var smart = GetSimpleFormatter();
smart.Settings.Formatter.AlignmentFillCharacter = '.'; // dot instead of space
var result = smart.Format("Name: {name,-10}|", obj);
Assert.That(result, Is.EqualTo("Name: Joe.......|"));
}

[Test]
public void Formatter_AlignNull()
{
var smart = GetSimpleFormatter();
string? name = null;
var obj = new { name = name };
var result = GetSimpleFormatter().Format("Name: {name,-10}| Column 2", obj);
Assert.That(result, Is.EqualTo("Name: | Column 2"));
}

[TestCase(0)]
[TestCase(2)]
[TestCase(-2)]
[TestCase(10)]
[TestCase(-10)]
public void ChooseFormatter_Alignment(int alignment)
{
var smart = GetSimpleFormatter();
smart.FormatterExtensions.Add(new ChooseFormatter());

var data = new {Number = 2};
var format = $"{{Number,{alignment}:choose(1|2|3):one|two|three}}";
var result = smart.Format(format, data);
var expected = string.Format($"{{0,{alignment}}}", "two");
Assert.That(result, Is.EqualTo(expected));
}

/// <summary>
/// Arguments should be aligned, but spacers should not
/// </summary>
/// <param name="alignment"></param>
[TestCase(0)]
[TestCase(2)]
[TestCase(-2)]
[TestCase(10)]
[TestCase(-10)]
public void ListFormatter_NestedFormats_Alignment(int alignment)
{
var smart = GetSimpleFormatter();
smart.FormatterExtensions.Add(new ListFormatter(smart));

var items = new [] { "one", "two", "three" };
var result = smart.Format($"{{items,{alignment}:list:{{}}|, |, and }}", new { items }); // important: not only "items" as the parameter
var expected = string.Format($"{{0,{alignment}}}, {{1,{alignment}}}, and {{2,{alignment}}}", items);
Assert.That(result, Is.EqualTo(expected));
}

/// <summary>
/// Arguments should be aligned, but spacers should not
/// </summary>
/// <param name="format"></param>
/// <param name="expected"></param>
[TestCase("{0:list}", "System.Int32[]")]
[TestCase("{0,2:list:|}"," 1 2 3 4 5")]
[TestCase("{0,4:list:000|}"," 001 002 003 004 005")]
[TestCase("{0,3:list:|,}"," 1, 2, 3, 4, 5")]
[TestCase("{0,-3:list:||+ |}","1 2 3 4 + 5 ")]
[TestCase("{0,0:list:N2|, |, and }","1.00, 2.00, 3.00, 4.00, and 5.00")]
public void ListFormatter_UnnestedFormats_Alignment(string format, string expected)
{
var smart = GetSimpleFormatter();
smart.FormatterExtensions.Add(new ListFormatter(smart));

var args = new[] {1, 2, 3, 4, 5};
var result = smart.Format(CultureInfo.InvariantCulture, format, args);

Assert.That(result, Is.EqualTo(expected));
}
}
}
2 changes: 1 addition & 1 deletion src/SmartFormat.Tests/Core/EscapedLiteralTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void UnEscapeCharLiterals_General_Test(string input, string expected, boo
{
if (shouldThrow)
{
Assert.That( () => EscapedLiteral.UnEscapeCharLiterals('\\', input, 0, input.Length, true), Throws.ArgumentException.And.Message.Contains(expected));
Assert.That( () => EscapedLiteral.UnEscapeCharLiterals('\\', input, 0, input.Length, false), Throws.ArgumentException.And.Message.Contains(expected));
}
else
{
Expand Down
32 changes: 6 additions & 26 deletions src/SmartFormat.Tests/Core/FormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using NUnit.Framework;
using SmartFormat.Core.Formatting;
using SmartFormat.Core.Output;
using SmartFormat.Core.Parsing;
using SmartFormat.Core.Settings;
using SmartFormat.Extensions;
using SmartFormat.Tests.TestUtils;
Expand All @@ -14,15 +13,14 @@ namespace SmartFormat.Tests.Core
[TestFixture]
public class FormatterTests
{
private object[] errorArgs = new object[]{ new FormatDelegate(format => { throw new Exception("ERROR!"); } ) };
private readonly object[] _errorArgs = { new FormatDelegate(format => throw new Exception("ERROR!")) };

private SmartFormatter GetSimpleFormatter()
{
var formatter = new SmartFormatter();
formatter.FormatterExtensions.Add(new DefaultFormatter());
formatter.SourceExtensions.Add(new ReflectionSource(formatter));
formatter.SourceExtensions.Add(new DefaultSource(formatter));
formatter.Parser.AddAlphanumericSelectors();
return formatter;
}

Expand All @@ -47,7 +45,7 @@ public void Formatter_Throws_Exceptions()
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.ThrowError;

Assert.Throws<FormattingException>(() => formatter.Test("--{0}--", errorArgs, "--ERROR!--ERROR!--"));
Assert.Throws<FormattingException>(() => formatter.Test("--{0}--", _errorArgs, "--ERROR!--ERROR!--"));
}

[Test]
Expand All @@ -56,7 +54,7 @@ public void Formatter_Outputs_Exceptions()
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.OutputErrorInResult;

formatter.Test("--{0}--{0:ZZZZ}--", errorArgs, "--ERROR!--ERROR!--");
formatter.Test("--{0}--{0:ZZZZ}--", _errorArgs, "--ERROR!--ERROR!--");
}

[Test]
Expand All @@ -65,7 +63,7 @@ public void Formatter_Ignores_Exceptions()
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.Ignore;

formatter.Test("--{0}--{0:ZZZZ}--", errorArgs, "------");
formatter.Test("--{0}--{0:ZZZZ}--", _errorArgs, "------");
}

[Test]
Expand All @@ -74,33 +72,15 @@ public void Formatter_Maintains_Tokens()
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.MaintainTokens;

formatter.Test("--{0}--{0:ZZZZ}--", errorArgs, "--{0}--{0:ZZZZ}--");
formatter.Test("--{0}--{0:ZZZZ}--", _errorArgs, "--{0}--{0:ZZZZ}--");
}

[Test]
public void Formatter_Maintains_Object_Tokens()
{
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.MaintainTokens;
formatter.Test("--{Object.Thing}--", errorArgs, "--{Object.Thing}--");
}

[Test]
public void Formatter_Align()
{
string name = "Joe";
var obj = new { name = name };
var str2 = GetSimpleFormatter().Format("Name: {name,10}| Column 2", obj);
Assert.That(str2, Is.EqualTo("Name: Joe| Column 2"));
}

[Test]
public void Formatter_AlignNull()
{
string? name = null;
var obj = new { name = name };
var str2 = GetSimpleFormatter().Format("Name: {name,-10}| Column 2", obj);
Assert.That(str2, Is.EqualTo("Name: | Column 2"));
formatter.Test("--{Object.Thing}--", _errorArgs, "--{Object.Thing}--");
}

[Test]
Expand Down
98 changes: 28 additions & 70 deletions src/SmartFormat.Tests/Extensions/ListFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using SmartFormat.Core.Extensions;
using SmartFormat.Core.Formatting;
using SmartFormat.Extensions;
using SmartFormat.Tests.TestUtils;
using SmartFormat.Core.Parsing;

namespace SmartFormat.Tests.Extensions
{
Expand Down Expand Up @@ -51,7 +49,7 @@ public void List_of_anonymous_types_and_enumerables()
Persons = data.Where(p => p.Gender == "M")
};

Smart.Default.Parser.UseAlternativeEscapeChar('\\'); // mandatory for this test case because of consecutive curly braces
Smart.Default.Settings.UseStringFormatCompatibility = false; // mandatory for this test case because of consecutive curly braces
Smart.Default.Settings.Formatter.ErrorAction = SmartFormat.Core.Settings.FormatErrorAction.ThrowError;
Smart.Default.Settings.Parser.ErrorAction = SmartFormat.Core.Settings.ParseErrorAction.ThrowError;

Expand All @@ -66,68 +64,38 @@ public void List_of_anonymous_types_and_enumerables()
Assert.AreEqual("Person A, Person C", result);
}

[Test]
public void FormatTest()
[TestCase("{4}", "System.Int32[]")]
[TestCase("{4:|}","12345")]
[TestCase("{4:00|}","0102030405")]
[TestCase("{4:|,}","1,2,3,4,5")]
[TestCase("{4:|, |, and }","1, 2, 3, 4, and 5")]
[TestCase("{4:N2|, |, and }","1.00, 2.00, 3.00, 4.00, and 5.00")]
public void FormatTest(string format, string expected)
{
var formats = new string[] {
"{4}",
"{4:|}",
"{4:00|}",
"{4:|,}",
"{4:|, |, and }",
"{4:N2|, |, and }",
};
var expected = new string[] {
"System.Int32[]",
"12345",
"0102030405",
"1,2,3,4,5",
"1, 2, 3, 4, and 5",
"1.00, 2.00, 3.00, 4.00, and 5.00",
};

var args = GetArgs();
Smart.Default.Test(formats, args, expected);
Smart.Default.Test(new[] {format}, args, new[] {expected});

}
[Test]
public void NestedFormatTest()
{
var formats = new string[] {
"{0:{}-|}",
"{0:{}|-}",
"{0:{}|-|+}",
"{0:({})|, |, and }",
};
var expected = new string[] {
"A-B-C-D-E-",
"A-B-C-D-E",
"A-B-C-D+E",
"(A), (B), (C), (D), and (E)",
};

[TestCase("{0:{}-|}", "A-B-C-D-E-")]
[TestCase("{0:{}|-}", "A-B-C-D-E")]
[TestCase("{0:{}|-|+}", "A-B-C-D+E")]
[TestCase("{0:({})|, |, and }", "(A), (B), (C), (D), and (E)")]
public void NestedFormatTest(string format, string expected)
{
var args = GetArgs();
Smart.Default.Test(formats, args, expected);
Smart.Default.Test(new[] {format}, args, new[] {expected});
}
[Test]
public void NestedArraysTest()
[TestCase("{2:{:{FirstName}}|, }", "Jim, Pam, Dwight")]
[TestCase("{3:{:M/d/yyyy} |}", "1/1/2000 10/10/2010 5/5/5555 ")]
[TestCase("{2:{:{FirstName}'s friends: {Friends:{FirstName}|, }}|; }", "Jim's friends: Dwight, Michael; Pam's friends: Dwight, Michael; Dwight's friends: Michael")]
public void NestedArraysTest(string format, string expected)
{
var formats = new string[] {
"{2:{:{FirstName}}|, }",
"{3:{:M/d/yyyy} |}",
"{2:{:{FirstName}'s friends: {Friends:{FirstName}|, } }|; }",
};
var expected = new string[] {
"Jim, Pam, Dwight",
"1/1/2000 10/10/2010 5/5/5555 ",
"Jim's friends: Dwight, Michael ; Pam's friends: Dwight, Michael ; Dwight's friends: Michael ",
};

var args = GetArgs();
Smart.Default.Test(formats, args, expected);
Smart.Default.Test(new[] {format}, args, new[] {expected});
}

[Test] /* added due to problems with [ThreadStatic] see: https://github.com/scottrippey/SmartFormat.NET/pull/23 */
[Test] /* added due to problems with [ThreadStatic] see: https://github.com/axuno/SmartFormat.NET/pull/23 */
public void WithThreadPool_ShouldNotMixupCollectionIndex()
{
// Old test did not show wrong Index value - it ALWAYS passed even when using ThreadLocal<int> or [ThreadStatic] respectively:
Expand Down Expand Up @@ -181,24 +149,14 @@ public void Objects_Not_Implementing_IList_Are_Not_Processed()
});
}

[Test]
public void TestIndex()
[TestCase("{0:{} = {Index}|, }", "A = 0, B = 1, C = 2, D = 3, E = 4")] // Index holds the current index of the iteration
[TestCase("{1:{Index}: {ToCharArray:{} = {Index}|, }|; }", "0: O = 0, n = 1, e = 2; 1: T = 0, w = 1, o = 2; 2: T = 0, h = 1, r = 2, e = 3, e = 4; 3: F = 0, o = 1, u = 2, r = 3; 4: F = 0, i = 1, v = 2, e = 3")] // Index can be nested
[TestCase("{0:{} = {1.Index}|, }", "A = One, B = Two, C = Three, D = Four, E = Five")] // Index is used to synchronize 2 lists
[TestCase("{Index}", "-1")] // Index can be used out-of-context, but should always be -1
public void TestIndex(string format, string expected)
{
var formats = new string[] {
"{0:{} = {Index}|, }", // Index holds the current index of the iteration
"{1:{Index}: {ToCharArray:{} = {Index}|, }|; }", // Index can be nested
"{0:{} = {1.Index}|, }", // Index is used to synchronize 2 lists
"{Index}", // Index can be used out-of-context, but should always be -1
};
var expected = new string[] {
"A = 0, B = 1, C = 2, D = 3, E = 4",
"0: O = 0, n = 1, e = 2; 1: T = 0, w = 1, o = 2; 2: T = 0, h = 1, r = 2, e = 3, e = 4; 3: F = 0, o = 1, u = 2, r = 3; 4: F = 0, i = 1, v = 2, e = 3",
"A = One, B = Two, C = Three, D = Four, E = Five",
"-1",
};

var args = GetArgs();
Smart.Default.Test(formats, args, expected);
Smart.Default.Test(new[] {format}, args, new[] {expected});
}
}
}
1 change: 1 addition & 0 deletions src/SmartFormat.Tests/Utilities/FormatDelegateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public void FormatDelegate_WithCulture_WithSmartFormat()
{
var amount = (decimal) 123.456;
var c = new CultureInfo("fr-FR");
// Only works for indexed placeholders
var formatDelegate = new FormatDelegate((text, culture) => GetAnswer("The amount is: ", amount, c));
Assert.That(Smart.Format("{0}", formatDelegate)
, Is.EqualTo($"The amount is: {amount.ToString(c)}"));
Expand Down
6 changes: 4 additions & 2 deletions src/SmartFormat/Core/Extensions/IFormattingInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the MIT license.
//

using System;
using System.ComponentModel;
using SmartFormat.Core.Formatting;
using SmartFormat.Core.Parsing;
Expand Down Expand Up @@ -68,9 +69,10 @@ public interface IFormattingInfo
void Write(string text, int startIndex, int length);

/// <summary>
/// Writes the nested format to the output.
/// Creates a child <see cref="IFormattingInfo"/> from the current <see cref="IFormattingInfo"/> instance
/// and invokes formatting with <see cref="SmartFormatter"/> with the child as parameter.
/// </summary>
void Write(Format format, object value);
void FormatAsChild(Format format, object value);

/// <summary>
/// Creates a <see cref="FormattingException" /> associated with the <see cref="IFormattingInfo.Format" />.
Expand Down
Loading

0 comments on commit d6e2a26

Please sign in to comment.