Tomas Voracek
Tomas Voracek

Reputation: 5914

Replace named group in regex with value

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?

Upvotes: 9

Views: 17170

Answers (8)

user1817787
user1817787

Reputation: 399

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(groupName, replacement, m);
        });
    }

    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:

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

Upvotes: 22

Base33
Base33

Reputation: 3215

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.

Upvotes: 0

Bastien Vandamme
Bastien Vandamme

Reputation: 18485

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);
    }

Upvotes: -1

GreatBittern
GreatBittern

Reputation: 218

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;
}

Upvotes: 1

Alex Basile
Alex Basile

Reputation: 1

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)));

Upvotes: 0

Danny Varod
Danny Varod

Reputation: 18069

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();
}

Upvotes: 1

Justin
Justin

Reputation: 6549

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;
}

Upvotes: 6

Guffa
Guffa

Reputation: 700182

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");

Upvotes: 13

Related Questions