Search code examples
c#template-engine

How to replace tokens on a string template?


I'm attempting to learn to write a basic template engine implementation. For example I have a string:

string originalString = "The current Date is: {{Date}}, the time is: {{Time}}";

what is the best way of reading the contents of each {{}} and then replacing the whole token with the valid string?

EDIT: Thanks to BrunoLM for pointing me in the right direction, so far this is what I have and it parses just fine, is there any other things I can do to optimize this function?

private const string RegexIncludeBrackets = @"{{(.*?)}}";

public static string ParseString(string input)
{
    return Regex.Replace(input, RegexIncludeBrackets, match =>
    {
        string cleanedString = match.Value.Substring(2, match.Value.Length - 4).Replace(" ", String.Empty);
        switch (cleanedString)
        {
            case "Date":
                return DateTime.Now.ToString("yyyy/MM/d");
            case "Time":
                return DateTime.Now.ToString("HH:mm");
            case "DateTime":
                return DateTime.Now.ToString(CultureInfo.InvariantCulture);
            default:
                return match.Value;
        }
    });
}

Solution

  • Short answer

    I think it would be best to use a Regex.

    var result = Regex.Replace(str, @"{{(?<Name>[^}]+)}}", m =>
    {
        return m.Groups["Name"].Value; // Date, Time
    });
    

    On you can use:

    string result = $"Time: {DateTime.Now}";
    

    String.Format & IFormattable

    However, there is a method for that already. Documentation.

    String.Format("The current Date is: {0}, the time is: {1}", date, time);
    

    And also, you can use a class with IFormattable. I didn't do performance tests but this one might be fast:

    public class YourClass : IFormattable
    {
        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (format == "Date")
                return DateTime.Now.ToString("yyyy/MM/d");
            if (format == "Time")
                return DateTime.Now.ToString("HH:mm");
            if (format == "DateTime")
                return DateTime.Now.ToString(CultureInfo.InvariantCulture);
    
            return format;
    
            // or throw new NotSupportedException();
        }
    }
    

    And use as

    String.Format("The current Date is: {0:Date}, the time is: {0:Time}", yourClass);
    

    Review of your code and detailed information

    In your current code you are using

    // match.Value = {{Date}}
    match.Value.Substring(2, match.Value.Length - 4).Replace(" ", String.Empty);
    

    Instead, if you look at my code above, I used the pattern

    @"{{(?<Name>[^}]+)}}"
    

    The syntax (?<SomeName>.*) means this is a named group, you can check the documentation here.

    It allows you to access match.Groups["SomeName"].Value which will be equivalent to the pattern with this group. So it would match two times, returning "Date" and then "Time", so you don't need to use SubString.

    Updating your code, it would be

    private const string RegexIncludeBrackets = @"{{(?<Param>.*?)}}";
    
    public static string ParseString(string input)
    {
        return Regex.Replace(input, RegexIncludeBrackets, match =>
        {
            string cleanedString = match.Groups["Param"].Value.Replace(" ", String.Empty);
            switch (cleanedString)
            {
                case "Date":
                    return DateTime.Now.ToString("yyyy/MM/d");
                case "Time":
                    return DateTime.Now.ToString("HH:mm");
                case "DateTime":
                    return DateTime.Now.ToString(CultureInfo.InvariantCulture);
                default:
                    return match.Value;
            }
        });
    }
    

    To improve even more, you can have a static compiled Regex field:

    private static Regex RegexTemplate = new Regex(@"{{(?<Param>.*?)}}", RegexOptions.Compiled);
    

    And then use as

    RegexTemplate.Replace(str, match => ...);