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.
1 Answer 1
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 useRegex.Replace
_logic
seems to be a poorly chosen name. While it describes the essence, but not its concrete job. You should nameFunc
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 inGroupCollection
, so we can skip writingm => m.Group...
, and theobject
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.
-
\$\begingroup\$ Thanks for the effort. These are some great suggestions. \$\endgroup\$Raul– Raul2018年01月26日 22:38:40 +00:00Commented Jan 26, 2018 at 22:38
[tT][oO][dD][aA][yY]
- you know that you could just use theRegexOptions.IgnoreCase
, don't you? \$\endgroup\$PlaceholderDefinition
that is of typeRegexOptions
? \$\endgroup\$