Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 51 additions & 13 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**<br/>
*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
Expand Down Expand Up @@ -267,29 +305,29 @@ _ = 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`
* `SystemTextJsonSource`

Fix: `NewtonSoftJsonSource` handles `null` values correctly ([#201](https://github.com/axuno/SmartFormat/pull/201))

### 16. `SmartFormatter` takes `IList<object>` parameters
### 17. `SmartFormatter` takes `IList<object>` parameters
Added support for `IList<object>` 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`.
Expand All @@ -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.
Expand All @@ -313,7 +351,7 @@ SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleS
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))
### 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.
Expand Down Expand Up @@ -367,7 +405,7 @@ SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleS
// result: "25 heures 1 minute"
```
<a id="ThreadSafety"></a>
### 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.

Expand All @@ -384,7 +422,7 @@ With `SmartSettings.IsThreadSafeMode=false` **should** be set for avoiding the m
The static `Smart.Format(...)` API overloads are allowed here.

<a id="ObjectPooling"></a>
### 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.

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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**

Expand Down
88 changes: 63 additions & 25 deletions src/SmartFormat.Tests/Extensions/SubStringFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace SmartFormat.Tests.Extensions
[TestFixture]
public class SubStringFormatterTests
{
private readonly List<object> _people;
private readonly object _person = new {Name = "Long John", City = "New York"};

private static SmartFormatter GetFormatter()
{
Expand All @@ -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<object>
{new {Name = "Long John", City = "New York"}, new {Name = "Short Mary", City = "Massachusetts"},};
var smart = GetFormatter();
Assert.Throws<FormattingException>(() => 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<SmartFormat.Core.Formatting.FormattingException>(() => smart.Format("No parameters: {Name:substr()}", _people.First()));
Assert.Throws<SmartFormat.Core.Formatting.FormattingException>(() => smart.Format("Only delimiter: {Name:substr(,)}", _people.First()));
Assert.Throws<FormattingException>(() => 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]
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -89,47 +95,44 @@ public void LengthLongerThanString_ThrowException()
{
var smart = GetFormatter();
var formatter = smart.GetFormatterExtension<SubStringFormatter>()!;
var behavior = formatter.OutOfRangeBehavior;

formatter.OutOfRangeBehavior = SubStringFormatter.SubStringOutOfRangeBehavior.ThrowException;
Assert.Throws<SmartFormat.Core.Formatting.FormattingException>(() => smart.Format("{Name:substr(0,999)}", _people.First()));

formatter.OutOfRangeBehavior = behavior;
Assert.Throws<FormattingException>(() => 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]
Expand All @@ -138,7 +141,19 @@ public void DataItemIsNull()
var smart = GetFormatter();
var ssf = smart.GetFormatterExtension<SubStringFormatter>();
ssf!.NullDisplayString = "???";
Assert.AreEqual(ssf.NullDisplayString, smart.Format("{Name:substr(0,3)}", new Dictionary<string, string?> { { "Name", null } }));
var result = smart.Format("{Name:substr(0,3)}", new KeyValuePair<string, object?>("Name", null));
Assert.That(result, Is.EqualTo(ssf.NullDisplayString));
}

[Test]
public void DataItemIsNull_With_ChildFormat()
{
var smart = GetFormatter();
var ssf = smart.GetFormatterExtension<SubStringFormatter>()!.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<string, object?>("Name", null));
Assert.That(result, Is.EqualTo("It is null"));
}

[Test]
Expand All @@ -148,19 +163,19 @@ public void Test_With_Changed_SplitChar()
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.AreEqual("Joh", smart.Format("{Name:substr(-4|-1)}", _person));
Assert.That(currentSplitChar, Is.EqualTo(',')); // make sure there was a change
}

[Test]
public void NamedFormatterWithoutOptionsShouldThrow()
{
var smart = GetFormatter();
Assert.That(() => smart.Format("{Name:substr()}", _people.First()), Throws.Exception.TypeOf<FormattingException>());
Assert.That(() => smart.Format("{Name:substr()}", _person), Throws.Exception.TypeOf<FormattingException>());
}

[Test]
public void NamedFormatterWithoutStringArgumentShouldThrow()
public void FormatterWithoutStringArgumentShouldThrow()
{
var smart = GetFormatter();
Assert.That(() => smart.Format("{0:substr(0,2)}", new object()), Throws.Exception.TypeOf<FormattingException>());
Expand All @@ -174,5 +189,28 @@ public void ImplicitFormatterEvaluation_With_Wrong_Args_Should_Fail()
smart.GetFormatterExtension<SubStringFormatter>()!.TryEvaluateFormat(
FormattingInfoExtensions.Create("{0::(0,2)}", new List<object?>(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<FormattingException>());
}

[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'"));
}
}
}
Loading