4
\$\begingroup\$

My requirement is to design a part of a solution in such matter that specific placeholders are replaces dynamically with a defined logic and new rules can be easily added.

Simple example:

Today is {today}. The year is {year}. Last year was {year-1}.

Should output something like:

Today is 26 Jan 2018. The year is 2018. Last year was 2017.

There are several more business and context specific placeholders and new rules can follow frequently.

As such I came up with this class:

public class PlaceholderDefinition
{
    public string Name { get; }
    public string Pattern { get; }
    private readonly Func<Match, string> _logic;

    public PlaceholderDefinition(string name, string pattern, Func<Match, string> logic)
    {
        Name = name;
        Pattern = pattern;
        _logic = logic;
    }

    public string Apply(Match match) => _logic.Invoke(match);
}

To fulfill the above example, I register the following PlaceholderDefinitions in the service configuration:

RegisteredPlaceholders.Add(
    new PlaceholderDefinition(
        "Today",
        @"{[tT][oO][dD][aA][yY]}",
        (_) => DateTime.Today.ToShortDateString()
    ));

RegisteredPlaceholders.Add(
    new PlaceholderDefinition(
        "Year",
        @"{[yY][eE][aA][rR]}",
        (_) => DateTime.Today.Year.ToString()
    ));

RegisteredPlaceholders.Add(
    new PlaceholderDefinition(
        "YearAddition",
        @"(?:{[yY][eE][aA][rR])([+-])(\d+)}",
        (m) =>
        {
            var operation = m.Groups[1].Value;
            int.TryParse(m.Groups[2].Value, out int value);
            return (operation == "+" ? DateTime.Today.Year + value : DateTime.Today.Year - value).ToString();
        }
    ));

The RegisteredPlaceholders is an enumerable of PlaceholderDefinition held in the container.

The logic of these patterns/placeholders is applied like this:

foreach (var placeholder in RegisteredPlaceholders)
 {
     var match = new Regex(placeholder.Pattern).Match(content);
     while (match.Success)
     {
         content = content.Remove(match.Index, match.Length).Insert(match.Index, placeholder.Apply(match));
         match = match.NextMatch();
     }
 }

I decided to take this approach instead of using Regex.Replace() to not reveal the content or text to the class defining the pattern logic. Maybe there is a more elegant solution, that I didn't come up with.

Every critique, improvement proposal, code smells are welcome.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ [tT][oO][dD][aA][yY] - you know that you could just use the RegexOptions.IgnoreCase, don't you? \$\endgroup\$
    – t3chb0t
    Commented Jan 26, 2018 at 21:07
  • \$\begingroup\$ I do but it applies to the complete pattern, in some matches case sensitivity is needed. \$\endgroup\$
    – Raul
    Commented Jan 26, 2018 at 21:14
  • 2
    \$\begingroup\$ Why don't you just add another parameter to the PlaceholderDefinition that is of type RegexOptions? \$\endgroup\$
    – t3chb0t
    Commented Jan 26, 2018 at 21:18
  • \$\begingroup\$ that is a good idea, I will do so \$\endgroup\$
    – Raul
    Commented Jan 26, 2018 at 21:30

1 Answer 1

3
\$\begingroup\$
  • Try to use named group. As your pattern becomes more complex, it will get harder to maintain.

    @"(?:{[yY][eE][aA][rR])([+-])(\d+)}"
    var operation = m.Groups[1].Value;
    int.TryParse(m.Groups[2].Value, out int value);
    
  • PlaceholderDefinition implementation requires way too much boilerplate code to use:

     foreach (var placeholder in RegisteredPlaceholders)
      {
          var match = new Regex(placeholder.Pattern).Match(content);
          while (match.Success)
          {
              content = content.Remove(match.Index, match.Length).Insert(match.Index, placeholder.Apply(match));
              match = match.NextMatch();
          }
      }
    

    It could be boiled down to: content = placeholder.Apply(text);

  • There is no need to use Match.NextMatch & string.Remove, when you can use Regex.Replace

  • _logic seems to be a poorly chosen name. While it describes the essence, but not its concrete job. You should name Func with "Selector, factory, builder, ...", and prefix it with a noun ("result, replacement, ...") and the private member prefix _ (if this is the convention, you choose to follow).

Modified PlaceholderDefinition class:

public class PlaceholderDefinition
{
    public string Name { get; }
    public Regex Pattern { get; }

    private readonly Func<GroupCollection, object> _replacementSelector;

    public PlaceholderDefinition(string name, string pattern, Func<GroupCollection, object> replacementSelector)
         : this(name, new Regex(pattern), replacementSelector)
    {
    }
    public PlaceholderDefinition(string name, string pattern, RegexOptions options, Func<GroupCollection, object> replacementSelector)
         : this(name, new Regex(pattern, options), replacementSelector)
    {
    }
    public PlaceholderDefinition(string name, Regex pattern, Func<GroupCollection, object> replacementSelector)
    {
        this.Name = name;
        this.Pattern = pattern;
        this._replacementSelector = replacementSelector;
    }

    public string Apply(string input) => Pattern.Replace(input, m => _replacementSelector(m.Groups).ToString());
}

There is various overloads of ctor to accommodate different needs, like the one suggested by @t3chb0t. Feels free to add more, if it help you to keep the declarations as clean as possible.

And, the rest of the code:

var text = "Today is {today}. The year is {year}. Last year was {year-1}.";
var placeholders = new List<PlaceholderDefinition>();
placeholders.Add(new PlaceholderDefinition("Today", @"{today}", RegexOptions.IgnoreCase, _ => DateTime.Today.ToShortDateString()));
placeholders.Add(new PlaceholderDefinition("Year", @"{year}", RegexOptions.IgnoreCase, _ => DateTime.Today.Year));
placeholders.Add(new PlaceholderDefinition(
    "YearAddition", 
    @"{year(?<sign>[+-])(?<value>\d+)}",
    RegexOptions.IgnoreCase, 
    g => DateTime.Today.Year + int.Parse(g["sign"].Value + g["value"].Value)
));

foreach (var placeholder in placeholders)
{
    text = placeholder.Apply(text);
}

  • The _replacementSelector takes in GroupCollection, so we can skip writing m => m.Group..., and the object TResult allows us to skip the necessary .ToString() or a parenthesis on binary operation.
  • int.Parse will parse number prefixed by a + sign as positive number, allowing us to skip the ternary operation.
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the effort. These are some great suggestions. \$\endgroup\$
    – Raul
    Commented Jan 26, 2018 at 22:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.