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;