From 07e63aea51abfec20f244a618cb7ff9aa8da4c75 Mon Sep 17 00:00:00 2001 From: axunonb Date: Tue, 8 Mar 2022 21:33:38 +0100 Subject: [PATCH] Added 'format' argument so the result of the substr can be processed --- CHANGES.md | 64 +++++++++++--- .../Extensions/SubStringFormatterTests.cs | 88 +++++++++++++------ .../Extensions/SubStringFormatter.cs | 51 ++++++++--- 3 files changed, 153 insertions(+), 50 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c39862e0..87b6a31e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -230,7 +230,45 @@ Smart.Format("{TheValue:isnull:The value is null|The value is {}}", new {TheValu // Result: "The value is 1234" ``` -### 12. Added `LocalizationFormatter` ([#176](https://github.com/axuno/SmartFormat/pull/207)) +### 12. Enhanced `SubStringFormatter` ([#258](https://github.com/axuno/SmartFormat/pull/258)) + +The only syntax in v2: +> **{ string : substr(*start*,*length*)** **}** + +Added optional *format-placeholder* in v3 +> **{ string : substr(*start*,*length*)** *: {format-placeholder}* **}** + +*value*: Only strings can be processed. Other objects cause a `FormattingException`. + +*arguments*: The start position and the lenght of the sub-string. + +**NEW**
+*format-placeholder*: A nested `Placeholder` that lets you format the result of the sub-string operation. + +#### Examples with Format argument + +The Format argument *must* contain nested `Placeholder`, and *may* contain literal text. + +**Convert the substring to lower-case** + +```CSharp +Smart.Format("{0:substr(0,2):{ToLower}}", "ABC"); +// | | +// + format + +// arg +// Outputs: "ab" +``` + +**Format the substring chars with the `ListFormatter`** + +Format the substring with *literal text* and *placeholder*. + +```CSharp +smart.Format("{0:substr(0,2):First 2 chars\\: {ToLower.ToCharArray:list:'{}'| and }}", "ABC"); +// Outputs: "First 2 chars: 'a' and 'b'" +``` + +### 13. Added `LocalizationFormatter` ([#176](https://github.com/axuno/SmartFormat/pull/207)) #### Features * Added `LocalizationFormatter` to localize literals and placeholders @@ -267,14 +305,14 @@ _ = Smart.Format("{0:plural:{:L(fr):{} item}|{:L(fr):{} items}}", 200; // result for French: 200 éléments ``` -### 13. Improved custom `ISource` and `IFormatter` implementations ([#180](https://github.com/axuno/SmartFormat/pull/180)) +### 14. Improved custom `ISource` and `IFormatter` implementations ([#180](https://github.com/axuno/SmartFormat/pull/180)) Any custom exensions can implement `IInitializer`. Then, the `SmartFormatter` will call `Initialize(SmartFormatter smartFormatter)` of the extension, before adding it to the extension list. -### 14. `IFormatter`s have one single, unique name ([#185](https://github.com/axuno/SmartFormat/pull/185)) +### 15. `IFormatter`s have one single, unique name ([#185](https://github.com/axuno/SmartFormat/pull/185)) In v2, `IFormatter`s could have an unlimited number of names. To improve performance, in v3, this is limited to one single, unique name. -### 15. JSON support ([#177](https://github.com/axuno/SmartFormat/pull/177), [#201](https://github.com/axuno/SmartFormat/pull/201)) +### 16. JSON support ([#177](https://github.com/axuno/SmartFormat/pull/177), [#201](https://github.com/axuno/SmartFormat/pull/201)) Separation of `JsonSource` into 2 `ISource` extensions: * `NewtonSoftJsonSource` @@ -282,14 +320,14 @@ Separation of `JsonSource` into 2 `ISource` extensions: Fix: `NewtonSoftJsonSource` handles `null` values correctly ([#201](https://github.com/axuno/SmartFormat/pull/201)) -### 16. `SmartFormatter` takes `IList` parameters +### 17. `SmartFormatter` takes `IList` parameters Added support for `IList` parameters to the `SmartFormatter` (thanks to [@karljj1](https://github.com/karljj1)) ([#154](https://github.com/axuno/SmartFormat/pull/154)) ### 18. `SmartObjects` have been removed * Removed obsolete `SmartObjects` (which have been replaced by `ValueTuple`) ([`092b7b1`](https://github.com/axuno/SmartFormat/commit/092b7b1b5873301bdfeb2b62f221f936efc81430)) -### 18. Improved parsing of HTML input ([#203](https://github.com/axuno/SmartFormat/pull/203)) +### 19. Improved parsing of HTML input ([#203](https://github.com/axuno/SmartFormat/pull/203)) Introduced experimental `bool ParserSettings.ParseInputAsHtml`. The default is `false`. @@ -303,7 +341,7 @@ Best results can only be expected with clean HTML: balanced opening and closing SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleSharp](https://anglesharp.github.io/) or [HtmlAgilityPack](https://html-agility-pack.net/). -### 19. Refactored `PluralLocalizationFormatter` ([#209](https://github.com/axuno/SmartFormat/pull/209)) +### 20. Refactored `PluralLocalizationFormatter` ([#209](https://github.com/axuno/SmartFormat/pull/209)) * Constructor with string argument for default language is obsolete. * Property `DefaultTwoLetterISOLanguageName` is obsolete. @@ -313,7 +351,7 @@ SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleS 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)) +### 21. 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)) * Constructor with string argument for default language is obsolete. * Property `DefaultTwoLetterISOLanguageName` is obsolete. @@ -367,7 +405,7 @@ SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleS // result: "25 heures 1 minute" ``` -### 21. Thread Safety ([#229](https://github.com/axuno/SmartFormat/pull/229)) +### 22. Thread Safety ([#229](https://github.com/axuno/SmartFormat/pull/229)) SmartFormat makes heavy use of caching and object pooling for expensive operations, which both require `static` containers. @@ -384,7 +422,7 @@ With `SmartSettings.IsThreadSafeMode=false` **should** be set for avoiding the m The static `Smart.Format(...)` API overloads are allowed here. -### 22. How to benefit from object pooling ([#229](https://github.com/axuno/SmartFormat/pull/229)) +### 23. How to benefit from object pooling ([#229](https://github.com/axuno/SmartFormat/pull/229)) In order to return "smart" objects back to the object pool, its important to use one of the following patterns. @@ -412,9 +450,9 @@ var resultString = smart.Format(parsedFormat); parsedFormat.Dispose(); ``` -### 23. Packages ([#238](https://github.com/axuno/SmartFormat/pull/238)) +### 24. Packages ([#238](https://github.com/axuno/SmartFormat/pull/238)) -#### 23.1 Package overview +#### 24.1 Package overview SmartFormat has the following NuGet packages: @@ -473,7 +511,7 @@ This package is a SmartFormat extension for reading and formatting `System.Xml.L This package is a SmartFormat extension for formatting `System.DateTime`, `System.DateTimeOffset` and `System.TimeSpan` types. -#### 23.2 Add extensions to the `SmartFormatter` +#### 24.2 Add extensions to the `SmartFormatter` **a) The easy way** diff --git a/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs b/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs index 22efd721..4ef1f3db 100644 --- a/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs @@ -11,7 +11,7 @@ namespace SmartFormat.Tests.Extensions [TestFixture] public class SubStringFormatterTests { - private readonly List _people; + private readonly object _person = new {Name = "Long John", City = "New York"}; private static SmartFormatter GetFormatter() { @@ -29,33 +29,39 @@ private static SmartFormatter GetFormatter() return smart; } - public SubStringFormatterTests() + [Test] + public void NoParentheses_Should_Work() + { + var smart = GetFormatter(); + Assert.AreEqual("No parentheses: Long John", smart.Format("No parentheses: {Name:substr}", _person)); + } + + [Test] + public void NoParameters_Should_Throw() { - _people = new List - {new {Name = "Long John", City = "New York"}, new {Name = "Short Mary", City = "Massachusetts"},}; + var smart = GetFormatter(); + Assert.Throws(() => smart.Format("Only delimiter: {Name:substr(,)}", _person)); } [Test] - public void NoParameters() + public void OnlyDelimiter_Should_Throw() { var smart = GetFormatter(); - Assert.AreEqual("No parentheses: Long John", smart.Format("No parentheses: {Name:substr}", _people.First())); - Assert.Throws(() => smart.Format("No parameters: {Name:substr()}", _people.First())); - Assert.Throws(() => smart.Format("Only delimiter: {Name:substr(,)}", _people.First())); + Assert.Throws(() => smart.Format("Only delimiter: {Name:substr(,)}", _person)); } [Test] public void StartPositionLongerThanString() { var smart = GetFormatter(); - Assert.AreEqual(string.Empty, smart.Format("{Name:substr(999)}", _people.First())); + Assert.AreEqual(string.Empty, smart.Format("{Name:substr(999)}", _person)); } [Test] public void StartPositionAndLengthLongerThanString() { var smart = GetFormatter(); - Assert.AreEqual(string.Empty, smart.Format("{Name:substr(999,1)}", _people.First())); + Assert.AreEqual(string.Empty, smart.Format("{Name:substr(999,1)}", _person)); } [Test] @@ -66,7 +72,7 @@ public void LengthLongerThanString_ReturnEmptyString() var behavior = formatter.OutOfRangeBehavior; formatter.OutOfRangeBehavior = SubStringFormatter.SubStringOutOfRangeBehavior.ReturnEmptyString; - Assert.AreEqual(string.Empty, smart.Format("{Name:substr(0,999)}", _people.First())); + Assert.AreEqual(string.Empty, smart.Format("{Name:substr(0,999)}", _person)); formatter.OutOfRangeBehavior = behavior; } @@ -79,7 +85,7 @@ public void LengthLongerThanString_ReturnStartIndexToEndOfString() var behavior = formatter.OutOfRangeBehavior; formatter.OutOfRangeBehavior = SubStringFormatter.SubStringOutOfRangeBehavior.ReturnStartIndexToEndOfString; - Assert.AreEqual("Long John", smart.Format("{Name:substr(0,999)}", _people.First())); + Assert.AreEqual("Long John", smart.Format("{Name:substr(0,999)}", _person)); formatter.OutOfRangeBehavior = behavior; } @@ -89,47 +95,44 @@ public void LengthLongerThanString_ThrowException() { var smart = GetFormatter(); var formatter = smart.GetFormatterExtension()!; - var behavior = formatter.OutOfRangeBehavior; formatter.OutOfRangeBehavior = SubStringFormatter.SubStringOutOfRangeBehavior.ThrowException; - Assert.Throws(() => smart.Format("{Name:substr(0,999)}", _people.First())); - - formatter.OutOfRangeBehavior = behavior; + Assert.Throws(() => smart.Format("{Name:substr(0,999)}", _person)); } [Test] public void OnlyPositiveStartPosition() { var smart = GetFormatter(); - Assert.AreEqual("John", smart.Format("{Name:substr(5)}", _people.First())); + Assert.AreEqual("John", smart.Format("{Name:substr(5)}", _person)); } [Test] public void StartPositionAndPositiveLength() { var smart = GetFormatter(); - Assert.AreEqual("New", smart.Format("{City:substr(0,3)}", _people.First())); + Assert.AreEqual("New", smart.Format("{City:substr(0,3)}", _person)); } [Test] public void OnlyNegativeStartPosition() { var smart = GetFormatter(); - Assert.AreEqual("John", smart.Format("{Name:substr(-4)}", _people.First())); + Assert.AreEqual("John", smart.Format("{Name:substr(-4)}", _person)); } [Test] public void NegativeStartPositionAndPositiveLength() { var smart = GetFormatter(); - Assert.AreEqual("Jo", smart.Format("{Name:substr(-4, 2)}", _people.First())); + Assert.AreEqual("Jo", smart.Format("{Name:substr(-4, 2)}", _person)); } [Test] public void NegativeStartPositionAndNegativeLength() { var smart = GetFormatter(); - Assert.AreEqual("Joh", smart.Format("{Name:substr(-4, -1)}", _people.First())); + Assert.AreEqual("Joh", smart.Format("{Name:substr(-4, -1)}", _person)); } [Test] @@ -138,7 +141,19 @@ public void DataItemIsNull() var smart = GetFormatter(); var ssf = smart.GetFormatterExtension(); ssf!.NullDisplayString = "???"; - Assert.AreEqual(ssf.NullDisplayString, smart.Format("{Name:substr(0,3)}", new Dictionary { { "Name", null } })); + var result = smart.Format("{Name:substr(0,3)}", new KeyValuePair("Name", null)); + Assert.That(result, Is.EqualTo(ssf.NullDisplayString)); + } + + [Test] + public void DataItemIsNull_With_ChildFormat() + { + var smart = GetFormatter(); + var ssf = smart.GetFormatterExtension()!.NullDisplayString = "???"; + // If a nested format is used, it gets NULL, too. + // Then, NullDisplayString will not be output + var result = smart.Format("{Name:substr(0,3):{:isnull:It is null}}", new KeyValuePair("Name", null)); + Assert.That(result, Is.EqualTo("It is null")); } [Test] @@ -148,7 +163,7 @@ public void Test_With_Changed_SplitChar() 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.AreEqual("Joh", smart.Format("{Name:substr(-4|-1)}", _person)); Assert.That(currentSplitChar, Is.EqualTo(',')); // make sure there was a change } @@ -156,11 +171,11 @@ public void Test_With_Changed_SplitChar() public void NamedFormatterWithoutOptionsShouldThrow() { var smart = GetFormatter(); - Assert.That(() => smart.Format("{Name:substr()}", _people.First()), Throws.Exception.TypeOf()); + Assert.That(() => smart.Format("{Name:substr()}", _person), Throws.Exception.TypeOf()); } [Test] - public void NamedFormatterWithoutStringArgumentShouldThrow() + public void FormatterWithoutStringArgumentShouldThrow() { var smart = GetFormatter(); Assert.That(() => smart.Format("{0:substr(0,2)}", new object()), Throws.Exception.TypeOf()); @@ -174,5 +189,28 @@ public void ImplicitFormatterEvaluation_With_Wrong_Args_Should_Fail() smart.GetFormatterExtension()!.TryEvaluateFormat( FormattingInfoExtensions.Create("{0::(0,2)}", new List(new[] {new object()}))), Is.EqualTo(false)); } + + [Test] + public void Format_Without_Nesting_Should_Throw() + { + var smart = GetFormatter(); + Assert.That(() => smart.Format("{0:substr(0,2):just text}", "input"), Throws.Exception.TypeOf()); + } + + [Test] + public void SubString_Using_Simple_Format() + { + var smart = GetFormatter(); + var result = smart.Format("{0:substr(0,2):{ToLower}}", "ABC"); + Assert.That(result, Is.EqualTo("ab")); + } + + [Test] + public void SubString_Using_Complex_Format() + { + var smart = GetFormatter(); + var result = smart.Format("{0:substr(0,2):{ToLower.ToCharArray:list:'{}'| and }}", "ABC"); + Assert.That(result, Is.EqualTo("'a' and 'b'")); + } } } diff --git a/src/SmartFormat/Extensions/SubStringFormatter.cs b/src/SmartFormat/Extensions/SubStringFormatter.cs index 6b37d4c5..09710e6b 100644 --- a/src/SmartFormat/Extensions/SubStringFormatter.cs +++ b/src/SmartFormat/Extensions/SubStringFormatter.cs @@ -4,11 +4,13 @@ using System; using SmartFormat.Core.Extensions; +using SmartFormat.Core.Formatting; +using SmartFormat.Core.Parsing; namespace SmartFormat.Extensions { /// - /// Formatter to access part of a string. + /// Formatter lets you output part of an input string. /// public class SubStringFormatter : IFormatter { @@ -38,11 +40,15 @@ public char SplitChar /// /// Get or set the string to display for NULL values, defaults to . + /// + /// It will not be used, if a format option is provided to the formatter. + /// In this case, the child formatter must handle the NULL result. + /// /// public string NullDisplayString { get; set; } = string.Empty; /// - /// Get or set the behavior for when start index and/or length is too great, defaults to . + /// Get or set the behavior when start index and/or length are too big, defaults to . /// public SubStringOutOfRangeBehavior OutOfRangeBehavior { get; set; } = SubStringOutOfRangeBehavior.ReturnEmptyString; @@ -62,12 +68,35 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) } var currentValue = formattingInfo.CurrentValue?.ToString(); + + var substring = currentValue == null ? ReadOnlySpan.Empty : GetSubstring(currentValue.AsSpan(), parameters); + + var format = formattingInfo.Format; + // A format was supplied, so use it if valid + if (format is not null && format.Length > 0) + { + if (!format.HasNested) + throw new FormattingException(formattingInfo.Format, "The format requires a nested placeholder", + format.StartIndex); + + formattingInfo.FormatAsChild(format, currentValue == null ? null : substring.ToString()); + return true; + } + + // Just output the substring directly if (currentValue == null) { formattingInfo.Write(NullDisplayString); return true; } - + + formattingInfo.Write(substring); + + return true; + } + + private ReadOnlySpan GetSubstring(ReadOnlySpan currentValue, string[] parameters) + { var (startPos, length) = GetStartAndLength(currentValue, parameters); switch(OutOfRangeBehavior) @@ -78,20 +107,18 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) break; case SubStringOutOfRangeBehavior.ReturnStartIndexToEndOfString: if (startPos + length > currentValue.Length) - length = (currentValue.Length - startPos); + length = currentValue.Length - startPos; break; } - var substring = parameters.Length > 1 - ? currentValue.Substring(startPos, length) - : currentValue.Substring(startPos); - - formattingInfo.Write(substring); - - return true; + // SubStringOutOfRangeBehavior.ThrowException: + // Without prior adjustments, this may throw + return parameters.Length > 1 + ? currentValue.Slice(startPos, length) + : currentValue.Slice(startPos); } - private static (int startPos, int length) GetStartAndLength(string currentValue, string[] parameters) + private static (int startPos, int length) GetStartAndLength(ReadOnlySpan currentValue, string[] parameters) { var startPos = int.Parse(parameters[0]); var length = parameters.Length > 1 ? int.Parse(parameters[1]) : 0;