Last active
August 3, 2017 08:29
-
-
Save GuerrillaCoder/c3a140ef759fd04b5307 to your computer and use it in GitHub Desktop.
C# Named Value String Format Replace For Templating
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a modification of https://mhusseini.wordpress.com/2014/05/03/fast-named-formats-in-c. | |
// To make it work as a flexible templating system I modified it so that it can use dynamic objects | |
// and silently removes unused fields. | |
// I am not sure how this has effected its performance. | |
using System; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
namespace StringExtension | |
{ | |
public static class NamedFormat | |
{ | |
// cache | |
private static readonly ConcurrentDictionary<string, object> PrecompiledExpressions = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase); | |
private static readonly Regex RegexFormatArgs = new Regex(@"([^{]|^){(\w+)}([^}]|$)|([^{]|^){(\w+)\:(.+)}([^}]|$)", RegexOptions.Compiled); | |
public static string Format<T>(string pattern, T item) | |
{ | |
var cacheKey = item.GetType().GetHashCode() + pattern; | |
// If we already have a compiled expression, just execute it. | |
object o; | |
if (PrecompiledExpressions.TryGetValue(cacheKey, out o)) | |
{ | |
return ((Func<T, string>)o)(item); | |
} | |
bool isIDictionary = (item is IDictionary<string, object>); | |
// Make an array of all property names to be checked against template | |
string[] keyArray = | |
isIDictionary | |
? ((IDictionary<string, object>)item).Keys.Select(x => x.ToString()).ToArray<string>() | |
: item.GetType().GetProperties().Select(x => x.Name).ToArray<string>(); | |
// Convert named format into regular format and return | |
// a list of the named arguments in order of appearance. | |
string replacedPattern; | |
var arguments = ParsePattern(pattern, out replacedPattern, keyArray); | |
// Now, construct code with Linq Expressions... | |
// We'll be using the String.Format method to actually perform the formating. | |
var formatMethod = typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) }); | |
// The constant that contains the format string: | |
var patternExpression = Expression.Constant(replacedPattern, typeof(string)); | |
// If its a non-dynamic class we can access object properties without conversion | |
var parameterExpression = Expression.Parameter(typeof(T), "static class"); | |
// If its a IDictionary then we need to perorm conversion so we can access value like a property | |
var castToDic = Expression.Convert(parameterExpression, typeof(IDictionary<string, object>)); | |
// create an array of property accessors | |
var argumentArrayElements = isIDictionary | |
? arguments.Select(argument => Expression.Convert(Expression.Property(castToDic, "Item", Expression.Constant(argument)), typeof(object))) // arguments.Select(s => ((IDictionary<string, object>)d)[s]) | |
: arguments.Select(argument => Expression.Convert(Expression.Property(parameterExpression, argument), typeof(object))); | |
var argumentArrayExpressions = Expression.NewArrayInit(typeof(object), argumentArrayElements); | |
// The actual call to String.Format: | |
var formatCallExpression = Expression.Call(formatMethod, patternExpression, argumentArrayExpressions); | |
// The lambda expression we will be compiling: | |
var lambdaExpression = Expression.Lambda<Func<T, string>>(formatCallExpression, parameterExpression); | |
// The lambda expression will look something like this | |
// input => string.Format("my format string", new[]{ input.Arg0, input.Arg1, ... }); or | |
// input => string.Format("my format string", new[]{ ((IDictionary<string,object>)input).Item.Arg0, ((IDictionary<string,object>)input).Item.Arg0}) | |
// Now we can compile the lambda expression | |
var func = lambdaExpression.Compile(); | |
// Cache the pre-compiled expression use type hash & pattern | |
PrecompiledExpressions.TryAdd(item.GetType().GetHashCode() + pattern, func); | |
// Execute the compiled expression | |
return func(item); | |
} | |
private static IEnumerable<string> ParsePattern(string pattern, out string replacedPattern, string[] objectKeys) | |
{ | |
// Just replace each named format items with regular format items | |
// and put all named format items in a list. Then return the | |
// new format string and the list of the named items. | |
var sb = new StringBuilder(); | |
var lastIndex = 0; | |
var arguments = new List<string>(); | |
var lowerarguments = new List<string>(); | |
foreach (var @group in from Match m in RegexFormatArgs.Matches(pattern) | |
select m.Groups[m.Groups[6].Success ? 5 : 2]) | |
{ | |
var key = @group.Value; | |
var lkey = key.ToLowerInvariant(); | |
var index = lowerarguments.IndexOf(lkey); | |
if (index < 0) | |
{ | |
index = lowerarguments.Count; | |
} | |
// if it is not in array silently remove it | |
if (!objectKeys.Contains(lkey, StringComparer.InvariantCultureIgnoreCase)) | |
{ | |
sb.Append(pattern.Substring(lastIndex, (@group.Index - 1) - lastIndex)); | |
lastIndex = @group.Index + (@group.Length + 1); | |
} | |
// otherwise replace it with index and add to arguments | |
else | |
{ | |
lowerarguments.Add(lkey); | |
arguments.Add(key); | |
sb.Append(pattern.Substring(lastIndex, @group.Index - lastIndex)); | |
sb.Append(index); | |
lastIndex = @group.Index + @group.Length; | |
} | |
} | |
sb.Append(pattern.Substring(lastIndex)); | |
replacedPattern = sb.ToString(); | |
return arguments; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment