Skip to content

Releases: axuno/SmartFormat

v3.2.0

18 Sep 21:26
596d0a9
Compare
Choose a tag to compare

Enhancements

  • Remove usage of Linq for less GC
  • Add IConvertable support for PluralLocalizationFormatter and ConditionalFormatter
  • ListFormatter
    • ListFormatter handles selector name "Index" in IEnumerables and ILists: In v1.6.1 a Selector was tested for having the name "index", even if data was not an IList, and returned the CollectionIndex. This is now implemented again in the ListFormatter.TryEvaluateSelector(...)
    • Set the ParentPlaceholder property for item Formats
    • Use PooledObject<T> where possible, so objects will be returned to ObjectPool also in case of exceptions

Fixes

  • FormatItem.AsSpan() returns the correct name
  • Remove potential namespace collisions: All public types in namespace Cysharp.Text are now internal

What's Changed

  • Upgrade projects targeting NET5.0 to NET6.0 by @axunonb in #286
  • Refactor suggestions by @karljj1 in #290
  • Refactored unit tests for ReflectionSource and thread-safe mode by @axunonb in #291
  • Convert solution to file-scoped namespacing by @axunonb in #292
  • Use is instead of checking the type info. by @karljj1 in #293
  • SonarCloud do not support analysis of forked PRs by @axunonb in #294
  • Avoid using Linq as it generates GC each time. by @karljj1 in #296
  • IConvertible support for plural and conditional formatters by @karljj1 in #300
  • Fixed: FormatItem.AsSpan() by @axunonb in #304
  • Refactor ListFormatter by @axunonb in #305
  • Ignore bools in plurals formatter. by @karljj1 in #306
  • Change all public types in namespace Cysharp.Text to internal by @axunonb in #309
  • ListFormatter handles selector name "Index" in IEnumerable and IList by @axunonb in #314
  • Bump version to v3.2.0 by @axunonb in #315

Full Changelog: v3.1.0...v3.2.0

v2.7.3

28 Jun 17:20
Compare
Choose a tag to compare

Fix: Newtonsoft.Json prior to version 13.0.1 is vulnerable

Newtonsoft.Json prior to version 13.0.1 is vulnerable to Insecure Defaults due to improper handling of expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM usage. Exploiting this vulnerability results in Denial Of Service (DoS).

Package reference updated to a minimum version 13.0.1

v3.1.0

17 May 20:42
511deaa
Compare
Choose a tag to compare

This is a feature update, that is released upon feedback from the community.

Thread-safe Mode

Thread-safe mode is now enabled by default:
SmartSettings.IsThreadSafeMode == true.

This has no impact on the API.

In case SmartFormat is exclusively utilized in a single-threaded context, SmartSettings.IsThreadSafeMode=false should be considered for enhanced performance.

Static Smart Methods for Formatting

Static Smart methods like Smart.Format(format, args) can now be called in an async / multi-threaded context.

The SmartFormatter instance returned by Smart.Default is flagged with the ThreadStatic attribute.
See more details in the Wiki: Async and Thread Safety

ListFormatter may have Placeholders in "spacers"

Thanks to karljj1 for the PR.

Before v3.1.0 the format options for ListFormatter could only contain literal text. Now Placeholders are allowed.

Example:

var args = new {
    Names = new[] { "John", "Mary", "Amy" },
    IsAnd = true, // true or false
    Split = ", "  // comma and space as list separator
};
_ = Smart.Format("{Names:list:{}|{Split}| {IsAnd:and|nor} }", args);
// Output for "IsAnd=true":  "John, Mary and Amy"
// Output for "IsAnd=false": "John, Mary nor Amy"

v3.0.0

14 Mar 10:08
ea5bcd7
Compare
Choose a tag to compare

Changes since v3.0.0-rc.2

SubStringFormatter

The formatter now accecpts a format argument with a nested Placeholder that lets you format the result of the sub-string operation.
Example: Convert the sub-string to lower-case:

Smart.Format("{0:substr(0,2):{ToLower}}", "ABC");

Reasoning: Enhancement

Nullable Notation

KeyValuePairSource, PersistentVariablesSource and GlobalVariablesSource can now process "null if nullable"
Reasoning: Enhancement

TemplateFormatter

The name of the formatter is now "t" (was "template" before).
Reasoning: Minimize the format string

Char to split options and formats is limited to pipe, comma, tilde

  • Char to split options and formats is limited to pipe, comma, tilde
  • SplitChar for formatters is unified and checked for validity
  • Affects ChooseFormatter, ConditionalFormatter, IsMatchFormatter, ListFormatter, PluralLocalizationFormatter, SubStringFormatter

Reasoning: Avoid conflicts with reserved characters

Modified ChooseFormatter case-sensitivity for option strings

  • bool and null: always case-insensitive
  • using SmartSettings.CaseSensitivity unless overridden with ChooseFormatter.CaseSensitivity
  • option strings comparison is culture-aware, using CultureInfo if supplied in Smart.Format(...) or CurrentUICulture

Reasoning: Enhancement

Magic Leading Colon

An implementation of a magic leading colon in order to identify whether ConditionalFormatter or PluralLocalizationFormatter should be invoked, was removed.
Reasoning:
This is undocumented and unnecessary.
Adding the formatter name in the format string achieves the same target.

v3.0.0-rc.2

15 Feb 08:26
3822589
Compare
Choose a tag to compare
v3.0.0-rc.2 Pre-release
Pre-release

Changes since v3.0.0-rc.1

1. Bugfix for Parser (#246)

A single escape character (\) at the end of the input string will now throw an ArgumentException with a comprehensive error message.

2. Added KeyValuePairSource (#244)

KeyValuePairSource isas a simple, cheap and performant way to create named placeholders.

Example:

Smart.Format("{placeholder}", new KeyValuePair<string, object?>("placeholder", "some value")
// Result: "some value"

3. Advanced features for IsMatchFormatter (#245)

The IsMatchFormatter is a formatter with evaluation of regular expressions.

New: The formatter can output matching group values of a RegEx.

Example:

We'll evalute this argument with IsMatchFormatter:

KeyValuePair<string, object> arg = new("theValue", "Some123Content");
a) Simple "match or no match" distinction:

This behavior is unchanged.

_ = Smart.Format("{theValue:ismatch(^.+123.+$):Okay - {}|No match content}", arg);
// Result: "Okay - Some123Content"

_ = Smart.Format("{theValue:ismatch(^.+999.+$):Okay - {}|No match content}", arg);
// Result: "No match content"
b) Show the matches in the output:
// List the matching RegEx group values
_ = Smart.Format("{theValue:ismatch(^.+\\(1\\)\\(2\\)\\(3\\).+$):Matches for '{}'\\: {m:list:| - }|No match}", arg);
// Result: "Matches for 'Some123Content': Some123Content - 1 - 2 - 3"

// Show specific matching RegEx group values with their index in the list
_ = Smart.Format("{theValue:ismatch(^.+\\(1\\)\\(2\\)\\(3\\).+$):First 2 matches in '{}'\\: {m[1]} and {m[2]}|No match}", arg);
// Result: "First 2 matches in 'Some123Content': 1 and 2"

The placeholder m is for the collection of matching RegEx group values generated by IsMatchFormatter. The collection has at least one entry for a successful match. See more details in the Microsoft docs for the GroupCollection class.

The name of the placeholder can be set with IsMatchFormatter.PlaceholderNameForMatches. "m" is the default.

4. Obsolete elements

Remainders from the v2.x API: All obsolete element usage creates a compile time error

v3.0.0-rc.1

01 Feb 23:35
ad32c30
Compare
Choose a tag to compare
v3.0.0-rc.1 Pre-release
Pre-release

Changes since v3.0.0-alpha.5

Packages (#238)

1. Package overview

SmartFormat has the following NuGet packages:

a) SmartFormat.NET

This is package which references all other packages below.

b) SmartFormat

SmartFormat is the core package. It comes with the most frequently used extensions built-in:

  1. Source extensions:
  • GlobalVariablesSource
  • PersistentVariablesSource
  • StringSource ✔️
  • ListFormatter (implementing ISource) ✔️
  • DictionarySource ✔️
  • ValueTupleSource ✔️
  • ReflectionSource ✔️
  • DefaultSource ✔️
  1. Formatter extensions:
  • ListFormatter (implementing IFormatter) ✔️
  • PluralLocalizationFormatter ✔️
  • ConditionalFormatter ✔️
  • IsMatchFormatter ✔️
  • NullFormatter ✔️
  • LocalizationFormatter
  • TemplateFormatter
  • ChooseFormatter ✔️
  • SubStringFormatter ✔️
  • DefaultFormatter ✔️

Breaking change:

Note that only extensions marked (✔️) are included when calling Smart.CreateDefaultFormatter(...). These default extensions differ from previous versions.

Some extensions (like PersistentVariablesSource and TemplateFormatter) require configuration to be useful.

c) SmartFormat.Extensions.System.Text.Json

This package is a SmartFormat extension for formatting System.Text.Json types as a source.

d) SmartFormat.Extensions.Newtonsoft.Json

This package is a SmartFormat extension for formatting Newtonsoft.Json types as a source.

e) SmartFormat.Extensions.Xml

This package is a SmartFormat extension for reading and formatting System.Xml.Linq.XElements.

f) SmartFormat.Extensions.Time

This package is a SmartFormat extension for formatting System.DateTime, System.DateTimeOffset and System.TimeSpan types.

2. Add extensions to the SmartFormatter

a) The easy way

Call Smart.CreateDefaultFormatter(...) and get a ready-to-use SmartFormatter. The same happens under the hood when calling one of the Smart.Format(...) methods.

b) The tailor-made alternative

When it comes to performance, it is advisable to add only those specific extensions that are needed. Just like this:

var formatter = new SmartFormatter()
    .AddExtensions(new ReflectionSource())
    .AddExtensions(new PluralLocalizationFormatter(), new DefaultFormatter());

Breaking change:

In v3.0 all WellKnownExtensionTypes.Sources and WellKnownExtensionTypes.Formatters are automatically inserted to the extension list at the place where they usually should be.

Any extension can, however, be inserted to the desired position in the extension list:

  • SmartFormatter.InsertExtension(int position, IFormatter sourceExtension)
  • SmartFormatter.InsertExtension(int position, IFormatter formatterExtension)

This can be useful especially when adding your custom extensions. You should call SmartFormatter.InsertExtension(...) after SmartFormatter.AddExtensions(...):

var formatter = new SmartFormatter()
    .AddExtensions(new ReflectionSource())
    .AddExtensions(new PluralLocalizationFormatter(), new DefaultFormatter())
    .InsertExtension(0, new MyCustomFormatter());

v2.7.2

26 Dec 21:14
bda5382
Compare
Choose a tag to compare
  • Fixed: ConditionalFormatter processes unsigned numbers in arguments correctly.
  • Fixed: JsonSource: Corrected handling of null values in Newtonsoft.Json objects.

v3.0.0-alpha.5

03 Dec 23:55
8df6c0d
Compare
Choose a tag to compare
v3.0.0-alpha.5 Pre-release
Pre-release

Changes since v.3.0.0-alpha.4

1. Object Pools

After implementing Object Pools for all classes which are frequently instantiated, GC and memory allocation again went down significantly.

In order to return "smart" objects back to the object pool, its important to use one of the following patterns.

Examples:

a) Single thread context (no need to care about object pooling)

var resultString = Smart.Format("format string", args);

b) Recommended: Auto-dispose Format (e.g.: caching, multi treading context)

var smart = Smart.CreateDefaultSmartFormat();
// Note "using" for auto-disposing the parsedFormat
using var parsedFormat = new Parser().ParseFormat("format string", args);
var resultString = smart.Format(parsedFormat);

c) Call Format.Dispose() (e.g.: caching, multi treading context)

var smart = Smart.CreateDefaultSmartFormat();
var parsedFormat = new Parser().ParseFormat("format string", args);
var resultString = smart.Format(parsedFormat);
// Don't use (or reference) "parsedFormat" after disposing
parsedFormat.Dispose();

2. Thread Safety

SmartFormat makes heavy use of caching and object pooling for expensive operations, which both require static containers.

a) Instantiating SmartFormatters from different threads:

`SmartSettings.IsThreadSafeMode=true` **must** be set, so that thread safe containers are used. This brings an inherent performance penalty.

 **Note:** The simplified `Smart.Format(...)` API overloads use a static `SmartFormatter` instance which is **not** thread safe. Call `Smart.CreateDefaultSmartFormat()` to create a default `Formatter`.

a) Instantiating SmartFormatters from a single thread:

`SmartSettings.IsThreadSafeMode=false` **should** be set for avoiding the multithreading overhead and thus for best performance. 

The simplified `Smart.Format(...)` API overloads are allowed here.

v3.0.0-alpha.4

25 Oct 20:27
3f76131
Compare
Choose a tag to compare
v3.0.0-alpha.4 Pre-release
Pre-release

Changes on top of v3.0.0-alpha.3:

1. Further improvement in performance

2. Added LocalizationFormatter (#176)

Features

  • Added LocalizationFormatter to localize literals and placeholders
  • Added ILocalizationProvider and a standard implemention as LocalizationProvider, which handles resx resource files. A fallback culture can be set. It will be used, in case no item for a certain culture could be found in any of the resources. LocalizationProvider can search an unlimited number of defined resoures.
  • SmartSettings were exended with category Localization. That way, custom IFormatters can also make use of localization, if needed.
  • Added LocalizationFormattingException, which is derived from FormattingException to easily identify this kind of issues

Examples

Culture-specific results shown here are included in embedded resource files, which are omitted for brevity.

a) Localize pure literals into Spanish:

// culture supplied as a format option
_ = Smart.Format(culture, "{:L(en):WeTranslateText}");
// culture supplied as an argument to the formatter
var culture = CultureInfo.GetCultureInfo("es");
_ = Smart.Format(culture, "{:L:WeTranslateText}");
// result for both: "Traducimos el texto"

b) Localized strings may contain placeholders

_ = Smart.Format("{0} {1:L(es):has {:#,#} inhabitants}", "X-City", 8900000);
// result: "X-City tiene 8.900.000 habitantes"
_ = Smart.Format("{0} {1:L(es):has {:#,#} inhabitants}", "X-City", 8900000);
// result: "X-City has 8,900,000 inhabitants"

c) Localization can be used together with other formatters

_ = Smart.Format("{0:plural:{:L(en):{} item}|{:L(en):{} items}}", 0;
// result for English: 0 items
_ = Smart.Format("{0:plural:{:L(fr):{} item}|{:L(fr):{} items}}", 0;
// result for French: 0 élément
_ = Smart.Format("{0:plural:{:L(fr):{} item}|{:L(fr):{} items}}", 200;
// result for French: 200 éléments

3. Refactored PluralLocalizationFormatter (#209)

  • Constructor with string argument for default language is obsolete.
  • Property DefaultTwoLetterISOLanguageName is obsolete.
  • Culture is now determined in this sequence (same as with LocalizationFormatter):

    a) Get the culture from the FormattingInfo.FormatterOptions.

    b) Get the culture from the IFormatProvider argument (which may be a CultureInfo) to SmartFormatter.Format(IFormatProvider, string, object?[])

    c) The CultureInfo.CurrentUICulture

4. Refactored TimeFormatter (#220, #221)

  • Constructor with string argument for default language is obsolete.
  • Property DefaultTwoLetterISOLanguageName is obsolete.
  • Culture is now determined in this sequence (same as with LocalizationFormatter and PluralLocalizationFormatter):

    a) Get the culture from the FormattingInfo.FormatterOptions.

    b) Get the culture from the IFormatProvider argument (which may be a CultureInfo) to SmartFormatter.Format(IFormatProvider, string, object?[])

    c) The CultureInfo.CurrentUICulture
  • New: With the extended CommonLanguagesTimeTextInfo, TimeFormatter includes French, Spanish, Portuguese, Italian and German as new languages besides English out-of-the-box.
  • New: With e.g. TimeFormatter.FallbackLanguage = "en";, this fallback language will be used, if no supported language could be found.
  • New: Custom languages can now easily be added to CommonLanguagesTimeTextInfo. Custom languages override built-in definitions.
    var language = "nl"; // dummy - it's English, not Dutch ;-)
    TimeTextInfo custom = new(
        pluralRule: PluralRules.GetPluralRule(language),
        week: new[] { "{0} week", "{0} weeks" },
        day: new[] { "{0} day", "{0} days" },
        hour: new[] { "{0} hour", "{0} hours" },
        minute: new[] { "{0} minute", "{0} minutes" },
        second: new[] { "{0} second", "{0} seconds" },
        millisecond: new[] { "{0} millisecond", "{0} milliseconds" },
        w: new[] { "{0}w" },
        d: new[] { "{0}d" },
        h: new[] { "{0}h" },
        m: new[] { "{0}m" },
        s: new[] { "{0}s" },
        ms: new[] { "{0}ms" },
        lessThan: "less than {0}");
    CommonLanguagesTimeTextInfo.AddLanguage(language, custom)
  • Changed:
    a) This notation - using formats as formatter options - was allowed in Smart.Format v2.x, but is now depreciated. It is still detected and working, as long as the format part is left empty
    var formatDepreciated = "{0:time(abbr hours noless)}";
    b) This format string is recommended for Smart.Format v3 and later. It allows for including the language as an option to the TimeFormatter:
    // Without language option:
    var formatRecommended = "{0:time:abbr hours noless:}";
    // With language option:
    var formatRecommended = "{0:time(en):abbr hours noless:}";
  • PRs for extending built-in languages are welcome.
  • Example:
    var timeSpan = new TimeSpan(1,1,1,1,1)
    Smart.Format("{0:time(en):hours minutes}", timeSpan);
    // result: "25 hours 1 minute"
    Smart.Format("{0:time(fr):hours minutes}", timeSpan);
    // result: "25 heures 1 minute"

v2.7.1

21 Oct 08:16
ca28b30
Compare
Choose a tag to compare

Changes

  • Fixed: #179 DualFromZeroToTwo plural rule. Thanks to @OhSoGood
  • Fixed: #211 Illegal placeholder characters that are not 8-bit, will no more throw unexpected ThrowByteOverflowException. Thanks to @bogatykh