Git Product home page Git Product logo

formatwith's Introduction

FormatWith

NuGet license Build status

A set of string extension methods for performing {named} {{parameterized}} string formatting, written for NetStandard 2.0.

Quick Info

This library provides named string formatting via the string extension .FormatWith(). It formats strings against a lookup dictionary, anonymous type, or handler.

It is written as a Net Standard 2.0 class library, published as a NuGet package, and is fully compatible with any .NET platform that implements NetStandard 2.0. This makes it compatible with .NET Core 2.0, .NET Full Framework 4.6.1, UWP/UAP 10, and most mono/xamarin platforms.

An example of what it can do:

using FormatWith;
...
string formatString = "Your name is {name}, and this is {{escaped}}, this {{{works}}}, and this is {{{{doubleEscaped}}}}";

// format the format string using the FormatWith() string extension.
// We can parse in replacement parameters as an anonymous type
string output = formatString.FormatWith(new { name = "John", works = "is good" });

// output now contains the formatted text.
Console.WriteLine(output);

Produces:

"Your name is John, and this is {escaped}, this {is good}, and this is {{doubleEscaped}}"

It can also be fed parameters via an IDictionary<string, string> or an IDictionary<string, object>, rather than a type.

The value of each replacement parameter is given by whatever the objects .ToString() method produces. This value is not cached, so you can get creative with the implementation (the object is fed directly into a StringBuilder).

How it works

A state machine parser quickly runs through the input format string, tokenizing the input into tokens of either "normal" or "parameter" text. These tokens are simply a struct with an index and length into the original format string - SubString() is avoided to prevent unnecessary string allocations. These are fed out of an enumerator right into a StringBuilder. Since StringBuilder is pre-allocated a small chunk of memory, and only .Append()ed relatively large segments of string, it produces the final output string quickly and efficiently.

Extension methods:

Three extension methods for string are defined in FormatWith.StringExtensions: FormatWith(), FormattableWith(), and GetFormatParameters().

FormatWith

The first, second, and third overload of FormatWith() take a format string containing named parameters, along with an object, dictionary, or function for providing replacement parameters. Optionally, missing key behaviour, a fallback value, and custom brace characters can be specified. Two adjacent opening or closing brace characters in the format string are treated as escaped, and will be reduced to a single brace in the output string.

Missing key behaviour is specified by the MissingKeyBehaviour enum, which can be ThrowException, ReplaceWithFallback, or Ignore.

ThrowException throws a KeyNotFoundException if a replacement value for a parameter in the format string could not be found.

ReplaceWithFallback inserts the value specified by fallbackReplacementValue in place of any parameters that could not be replaced. If an object-based overload is used, fallbackReplacementValue is an object, and the string representation of the object will be resolved as the value.

Ignore ignores any parameters that did not have a corresponding key in the lookup dictionary, leaving the unmodified braced parameter in the output string. This is useful for tiered formatting.

Examples:

`string output = "abc {Replacement1} {DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 });

output: Throws a KeyNotFoundException with the message "The parameter "DoesntExist" was not present in the lookup dictionary".

string output = "abc {Replacement1} {DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.ReplaceWithFallback, "FallbackValue");

output: "abc Replacement1 FallbackValue"

`string replacement = "abc {Replacement1} {DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore);

output: "abc Replacement1 {DoesntExist}"

Using custom brace characters:

Custom brace characters can be specified for both opening and closing parameters, if required.

string replacement = "abc <Replacement1> <DoesntExist>".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore, null,'<','>');

output: "abc Replacement1 "

FormattableWith

The first, second, and third overload of FormattableWith() function much the same way that the FormatWith() overloads do. However, FormattableWith returns a FormattableString instead of a string. This allows parameters and composite format string to be inspected, and allows a custom formatter to be used if desired.

Handler overloads

A custom handler can be passed to both FormatWith() and FormattableWith(). The handler is passed the value of each parameter key and format (if applicable). It is responsible for providing a ReplacementResult in response. The ReplacementResult contains the Value which will be substituted, as well as a boolean Success parameter indicating whether the replacement was successful. If Success is false, the MissingKeyBehaviour is followed, as per the other overloads of FormatWith.

This can allow for some neat tricks, and even complex behaviours.

Example:

"{abcDEF123:reverse}, {abcDEF123:uppercase}, {abcDEF123:lowercase}.".FormatWith(
            (parameter, format) =>
            {
                switch (format)
                {
                    case "uppercase":
                        return new ReplacementResult(true, parameter.ToUpper());
                    case "lowercase":
                        return new ReplacementResult(true, parameter.ToLower());
                    case "reverse":
                        return new ReplacementResult(true, new string(parameter.Reverse().ToArray()));
                    default:
                        return new ReplacementResult(false, parameter);
                }
            });

Produces:

"321FEDcba, ABCDEF123, abcdef123."

GetFormatParameters

GetFormatParameters() can be used to get a list of parameter names out of a format string, which can be used for inspecting a format string before performing other actions on it.

Example:

IEnumerable<string> parameters = "{parameter1} {parameter2} {{not a parameter}}".GetFormatParameters();

output: The enumerable will return "parameter1","parameter2" during iteration.

Tests:

A testing project is included that has coverage of most scenarios involving the three extension methods. The testing framework in use is xUnit.

Performance:

The SpeedTest test function performs 1,000,000 string formats, with a format string containing 1 parameter. On a low end 1.3Ghz mobile i7, this completes in around 700ms, giving ~1.4 million replacements per second.

The SpeedTestBigger test performs a more complex replacement on a longer string containing 2 parameters and several escaped brackets, again 1,000,000 times. On the same hardware, this test completed in around 1 seconds.

The SpeedTestBiggerAnonymous test is the same as SpeedTestBigger, but uses the anonymous function overload of FormatWith. It completes in just under 2 seconds. Using the anonymous overload of FormatWith is slightly slower due to reflection overhead, although this is minimised by caching.

So as a rough performance guide, FormatWith will usually manage about 1 million parameter replacements per second on low end hardware.

formatwith's People

Contributors

astef avatar crozone avatar jeremyskinner avatar jmaharman avatar vuchl avatar zacatkogan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

formatwith's Issues

Assembly signing / strong naming

Hi Ryan,

It would be good if the assembly could be signed.

Although strong naming is a PITA, Microsoft's recommendation for OSS projects it that OSS libraries should be signed, so that they can be used by both projects which are strong named, and those the aren't (if the assembly isn't strong-named, then it won't be able to be loaded by end-user projects which are strong named, especially in net4x which has strict assembly loading).

FluentValidation is strong named because of this, so FormatWith would need to be as well if we decide to take a dependency on it in the end.

Handle whitespace padding around parameters

Currently the tokenizer considers the entire contents of text between the open bracket and closed bracket the "key", including any whitespace that may be surrounding the actual text.

This is inconsistent with other formatters and the formatter built into the C# compiler.

Eg: abc{ Replacement1 } outputs the key " Replacement1 ", when it should really be "Replacement1".

Additionally, spaces are currently allowed in the middle of keys, which throws an "Unexpected Token" error in the C# compiler's string interpolation.

Both of these behaviours should eventually be configurable with a user specified setting value.

Items:

  • Write tests
  • Implement whitespace trimming in the tokenizer (Thanks Trim(ReadOnlySpan<Char>)
  • Implement whitespace detection in the key

[Feature Request] Add a type that tokenizes the string once, and reuse it for processing parameters.

Hi @crozone, thanks for this very useful and easy-to-use library.

I'm wondering if it would be possible to add support for something that can parse the parametrized string once and then just reuse the parsed tokens for all replacement iterations?

I just gave it a try here https://github.com/mohummedibrahim/FormatWith, please let me know if that's something you're willing to accept and I can make an initial PR?

Thanks again!

Formatting with SubProperties

I'm not sure how heavily inspired this is by the James Newton-Kings' FormatWith, but that would allow you to specify a SubProperty (the same as you can with Interpolated Strings).

e.g.

var foo = new { Property = new { SubProperty = "SubProperty" }};
var bar = "{Property.SubProperty}".FormatWith(foo);
var baz = $"{foo.Property.SubProperty}";

// bar = "SubProperty"
// baz = "SubProperty"

It doesn't look like it would be too hard to implement, just requires splitting the Key to get the Members.

Add support for indexers (support for Arrays/Lists/Dictionaries/etc)

At present, FormatWith supports basic property navigations in parameter keys. However, it does not support combining this with indexers to navigate into objects arrays, lists, or dictionaries.

For example, this is currently supported:

string result = "{foo.bar}".FormatWith(new 
{ 
    foo = new { bar = "test" }
});

However, we should be able to do this:

string result = "{foo[\"bar\"].Length}".FormatWith(new 
{
    foo = new Dictionary<string, string> { ["bar"] = "test" }
});

This is the code that will need to be changed:

private static bool TryGetPropertyFromObject(string key, object replacementObject, out object value)
{
// need to split this into accessors so we can traverse nested objects
var members = key.Split(new[] { "." }, StringSplitOptions.None);
if (members.Length == 1)
{
PropertyInfo propertyInfo = replacementObject.GetType().GetProperty(key, propertyBindingFlags);
if (propertyInfo == null)
{
value = null;
return false;
}
else
{
value = propertyInfo.GetValue(replacementObject);
return true;
}
}
else
{
object currentObject = replacementObject;
foreach (var member in members)
{
PropertyInfo propertyInfo = currentObject.GetType().GetProperty(member, propertyBindingFlags);
if (propertyInfo == null)
{
value = null;
return false;
}
else
{
currentObject = propertyInfo.GetValue(currentObject);
}
}
value = currentObject;
return true;
}
}

Additional formatting options.

As FormatWith gains features, additional options to control how formatting is performed will be required.

Control over disabling property and indexer navigations.

Property navigations, and soon indexers (#26), are supported in property keys.
In these situations, FormatWith will use reflection to introspect the input object and retrieve the required value.

However, a use-case for FormatWith is handling untrusted string inputs. For security reasons, it is desirable to be able to disable these features so that unstrusted inputs cannot navigate to properties that are not anticipated.

Additional templating formats

We may want to support additional templating styles and formats, such as multi-character start and end brackets (#24), or formats like mustache.js (#11). Selecting which format is desired will necessarily require another configuration option.

Options object

As FormatWith grows in complexity it will be desirable to encapsulate the format options into a dedicated object/struct and pass it in as a dedicated parameter, in order to reduce the number of parameters that .FormatWith() is called with.

Parameters like the openBraceChar and closeBraceChar would be rolled into this object.

Updated release?

Hi @crozone,

I'm the author of the FluentValidation library. We currently process named formats in our error messages using a regex, but I've been considering switching to use FormatWith as it performs better.

Unfortunately the current release on nuget doesn't support format arguments (such as "{foo:c3}"), which we require, but I notice that this was committed last year but hasn't been released.

I was wondering if this library is still under development and if you're planning on pushing out an updated release at any point? If not, would you mind if I forked the source code and made use of the parts that I need within FluentValidation?

Thanks!

Nested arguments

Does this library support nested arguments? i.e.:

"{foo.bar}".FormatWith(new { foo = new { bar = "test" } })

Vectorize the tokenizer

Currently the tokenizer is a simple for loop over the input string. This is simple to implement and understand, but it leaves some performance on the table. Vectorization (eg SIMD) can be used to more quickly search the input string for noteworthy symbols (open and closed brackets).

MemoryExtensions.IndexOfAny() has been internally vectorized in .NET 5. If the tokenizer is rewritten to use IndexOfAny() to jump forwards to the next noteworthy token, it can jump over strides of text much more quickly.

Breaking changes for 2.0 release

I have a few improvements to make to FormatWith 2.0 that will be have breaking changes. If anyone would like to provide feedback or comment, please do so here.

  1. Remove default parameters from method calls and replace with simple overloads.
    This appears to be a step backwards but is required to maintain CLR compliance and allow the library to be used in CLR languages that don't implement default values for method parameters. This will break the API contract in existing applications but likely won't require many code changes to allow your apps to recompile.

  2. Remove the Action<FormatToken, StringBuilder> based overload of FormatWith. This overload appears first in VS Intellisense yet I doubt it is ever really used. IMHO it's confusing, clutters the namespace, and provides very little utility.

  3. Upgrade project to new csproj format and ensure compatibility with .NET Core 2.0 when it's released.

  4. Add extra methods that are compatible with FormattableString
    FormattableString compatibility is a "nice to have" in frameworks that support it. If you have a library that supports FormattableString for use with C# 6's build in string interpolation, this would make Format With automatically compatible with that library.

  5. Release a fork that's compatible with .NET Compact Framework. I've been using a CF compatible fork internally for some time, but it's from a very old version.

Allow passing in StringComparer or StringComparison for object based lookups

Preface

It is currently possible to change the key equality behaviour when passing in a lookup Dictionary<string, T> by specifying a specific StringComparer in the dictionary constructor. This is passed into the dictionary constructor as an IEqualityComparer<string>, allowing the user to set this before calling .FormatWith.

For example:

var formatted = template.FormatWith(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
    ["key1"] = value1
});
  • (TODO: Add the above example to the README.md)

However, when using the object based lookup (eg with an anonymous type), the key lookup behaviour is specified internally and there is no way for the user to specify the StringComparer or StringComparison kind.

Tasks

  • Add extra arguments to the object based FormatWith() overloads to allow this string comparison to be changed.

Span<T> goodness

Need to investigate updating the parser to return ReadOnlySpan<T> instead of the current struct. Probably won't make much of a difference to performance, since the current implementation is already using structs on the stack, but it should be good for code style points.

Problem with GetFormatParameters()

Hi,

If I run:

IEnumerable<string> parameters = "{parameter1} {parameter2} {{not a parameter}}".GetFormatParameters();
Console.WriteLine(parameters);

The console output looks like:
System.Linq.Enumerable+WhereSelectEnumerableIterator`2[FormatWith.Internal.FormatToken,System.String]

Do you have any idea why?

Add Support for generic value dictionaries

Would be nice if the library supported dictionaries with generic types.

var inputs = new Dictionary<string, double>
{
    { "a", 1 },
    { "b", 2 }
};
var result = "{a},{b}".FormatWith(inputs, MissingKeyBehaviour.ThrowException);
// throws System.Collections.Generic.KeyNotFoundException

Define the CultureInfo of the format

We are using crozone/FormatWith to fill mail templates. These email templates must be sent in Spanish. However when trying to format the dates they appear in English.

Is there a mechanism to define the CultureInfo of the format?

            var template = "{d:dddd, MMMM dd, yyyy}";

            var stringDictionary = new Dictionary<string, object>
            {
                { "d", new DateTime(2025, 12, 31, 5, 10, 20) },
            };
            var result = template.FormatWith(template, stringDictionary);

            Assert.AreEqual("Wednesday, December 31, 2025", result);

Thanks in advance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.