9
votes

I want to use regular expression same way as string.Format. I will explain

I have:

string pattern = "^(?<PREFIX>abc_)(?<ID>[0-9])+(?<POSTFIX>_def)$";
string input = "abc_123_def";
Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
string replacement = "456";
Console.WriteLine(regex.Replace(input, string.Format("${{PREFIX}}{0}${{POSTFIX}}", replacement)));

This works, but i must provide "input" to regex.Replace. I do not want that. I want to use pattern for matching but also for creating strings same way as with string format, replacing named group "ID" with value. Is that possible?

I'm looking for something like:

string pattern = "^(?<PREFIX>abc_)(?<ID>[0-9])+(?<POSTFIX>_def)$";
string result = ReplaceWithFormat(pattern, "ID", 999);

Result will contain "abc_999_def". How to accomplish this?

8
This isn't exactly what you are looking for, but may be of value. It's basicly a named string.format, instead of indexed. haacked.com/archive/2009/01/04/…chilltemp

8 Answers

20
votes

Yes, it is possible:

public static class RegexExtensions
{
    public static string Replace(this string input, Regex regex, string groupName, string replacement)
    {
        return regex.Replace(input, m =>
        {
            return ReplaceNamedGroup(input, groupName, replacement, m);
        });
    }

    private static string ReplaceNamedGroup(string input, string groupName, string replacement, Match m)
    {
        string capture = m.Value;
        capture = capture.Remove(m.Groups[groupName].Index - m.Index, m.Groups[groupName].Length);
        capture = capture.Insert(m.Groups[groupName].Index - m.Index, replacement);
        return capture;
    }
}

Usage:

Regex regex = new Regex("^(?<PREFIX>abc_)(?<ID>[0-9]+)(?<POSTFIX>_def)$");

string oldValue = "abc_123_def";
var result = oldValue.Replace(regex, "ID", "456");

Result is: abc_456_def

13
votes

No, it's not possible to use a regular expression without providing input. It has to have something to work with, the pattern can not add any data to the result, everything has to come from the input or the replacement.

Intead of using String.Format, you can use a look behind and a look ahead to specify the part between "abc_" and "_def", and replace it:

string result = Regex.Replace(input, @"(?<=abc_)\d+(?=_def)", "999");
6
votes

There was a problem in user1817787 answer and I had to make a modification to the ReplaceNamedGroup function as follows.

private static string ReplaceNamedGroup(string input, string groupName, string replacement, Match m)
{
    string capture = m.Value;
    capture = capture.Remove(m.Groups[groupName].Index - m.Index, m.Groups[groupName].Length);
    capture = capture.Insert(m.Groups[groupName].Index - m.Index, replacement);
    return capture;
}
1
votes

Another edited version of the original method by @user1817787, this one supports multiple instances of the named group (also includes similar fix to the one @Justin posted (returns result using {match.Index, match.Length} instead of {0, input.Length})):

public static string ReplaceNamedGroup(
    string input, string groupName, string replacement, Match match)
{
    var sb = new StringBuilder(input);
    var matchStart = match.Index;
    var matchLength = match.Length;

    var captures = match.Groups[groupName].Captures.OfType<Capture>()
        .OrderByDescending(c => c.Index);

    foreach (var capt in captures)
    {
        if (capt == null)
            continue;

        matchLength += replacement.Length - capt.Length;

        sb.Remove(capt.Index, capt.Length);
        sb.Insert(capt.Index, replacement);
    }

    var end = matchStart + matchLength;
    sb.Remove(end, sb.Length - end);
    sb.Remove(0, matchStart);

    return sb.ToString();
}
1
votes

I shortened ReplaceNamedGroup, still supporting multiple captures.

private static string ReplaceNamedGroup(string input, string groupName, string replacement, Match m)
{
  string result = m.Value;
  foreach (Capture cap in m.Groups[groupName]?.Captures)
  {
    result = result.Remove(cap.Index - m.Index, cap.Length);
    result = result.Insert(cap.Index - m.Index, replacement);
  }
return result;
}
0
votes

The simple solution is to refer to the matched groups in replacement. So the Prefix is $1 and Postfix is $3.

I've haven't tested the code below but should work similar to a regEx I've written recently:

string pattern = "^(?<PREFIX>abc_)(?<ID>[0-9])+(?<POSTFIX>_def)$";
string input = "abc_123_def";
Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
string replacement = String.Format("$1{0}$3", "456");
Console.WriteLine(regex.Replace(input, string.Format("${{PREFIX}}{0}${{POSTFIX}}", replacement)));
0
votes

In case this helps anyone, I enhanced the answer with the ability to replace multiple named capture groups in one go, which this answer helped massively to achieve.

public static class RegexExtensions
    {
        public static string Replace(this string input, Regex regex, Dictionary<string, string> captureGroupReplacements)
        {
            string temp = input;

            foreach (var key in captureGroupReplacements.Keys)
            {
                temp = regex.Replace(temp, m =>
                {
                    return ReplaceNamedGroup(key, captureGroupReplacements[key], m);
                });
            }
            return temp;
        }

        private static string ReplaceNamedGroup(string groupName, string replacement, Match m)
        {
            string capture = m.Value;
            capture = capture.Remove(m.Groups[groupName].Index - m.Index, m.Groups[groupName].Length);
            capture = capture.Insert(m.Groups[groupName].Index - m.Index, replacement);
            return capture;
        }
    }

Usage:

var regex = new Regex(@"C={BasePath:""(?<basePath>[^\""].*)"",ResultHeadersPath:""ResultHeaders"",CORS:(?<cors>true|false)");

content = content.Replace(regex, new Dictionary<string, string>
{
   { "basePath", "www.google.com" },
   { "cors", "false" }
};

All credit should go to user1817787 for this one.

-1
votes

You should check the documentation about RegEx replace here

I created this to replace a named group. I cannot use solution that loop on all groups name because I have case where not all expression is grouped.

    public static string ReplaceNamedGroup(this Regex regex, string input, string namedGroup, string value)
    {
        var replacement = Regex.Replace(regex.ToString(), 
            @"((?<GroupPrefix>\(\?)\<(?<GroupName>\w*)\>(?<Eval>.[^\)]+)(?<GroupPostfix>\)))", 
            @"${${GroupName}}").TrimStart('^').TrimEnd('$');
        replacement = replacement.Replace("${" + namedGroup + "}", value);
        return Regex.Replace(input, regex.ToString(), replacement);
    }