Need to extract text in a C# Regex

Viewed 78

I have a collection of strings such as Some song [FEAT. John Doe] and I'm trying to extract the 'featured' part. It could be identified by one of several different words "FEAT|FEAT\\.|Featuring" and may or may not be enclosed by brackets. I'm using a Regex for this and here is what I've got so far:

[TestMethod]
public void ExtractFeaturedPerformers()
{
    IEnumerable<string> titles = new string[]
    {
        "abc [FEAT one two] 123",
        "def(FEAT. three'four) 456",
        "ghi Featuring five",
        "jkl"
    };

    // Must be able to use an arbitrary array of words
    var arrayOfWords = new string[] { "FEAT", "FEAT.", "Featuring" };
    string options = string.Join("|", arrayOfWords.Select(s => Regex.Escape(s)));
    var result = new List<string>();

    foreach(string title in titles)
    {
        var _ = Regex.Match(title, $@"(?<=({options})\s*)(.*?)(?=[\]\)]|$)");
        if (_.Success)
            result.Add(_.Value);
    }

    Assert.AreEqual(3, result.Count());
    Assert.IsTrue(result.Contains("one two"));
    Assert.IsTrue(result.Contains("three'four"));
    Assert.IsTrue(result.Contains("five"));
}

I have it mostly working but there are two limitations. My main problem is that the second result includes the ..

. three'four

How can I remove this as part of the Regex so that I can accept an arbitrary options string rather than stripping it away later? Dealing with the . is my main concern but I would also appreciate suggestions for removing the leading and trailing whitespace from the result so that I don't have to call Trim() later.

1 Answers

You need

(?:FEAT\.?|Featuring)\s*([^])]*)

See the regex demo

Details

  • (?:FEAT\.?|Featuring) - FEAT and an optional . or Featuring
  • \s* - zero or more whitespace
  • ([^])]*) - Capturing group 1: zero or more chars other than ] and ).

You need to amend the C# code to get Group 1 values.

Here is the full C# demo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public class Test
{
    public static void Main()
    {
        IEnumerable<string> titles = new string[]
        {
            "abc [FEAT one two] 123",
            "def(FEAT. three'four) 456",
            "ghi Featuring five",
            "jkl"
        };

        var keys = new List<string> { "FEAT", "FEAT.", "Featuring" };
        keys = keys.OrderByDescending(x => x.Length).ToList();
        var pattern = $@"(?:{string.Join("|", keys.Select(z => Regex.Escape(z)))})\s*([^])]*)";
        Console.WriteLine(pattern);
        
        var result = new List<string>();
        foreach(string title in titles)
        {
            var _ = Regex.Match(title, pattern);
            if (_.Success)
                result.Add(_.Groups[1].Value);
        }
    
        Console.WriteLine( result.Count()); // Assert.AreEqual(3, result.Count());
        Console.WriteLine( result.Contains("one two") ); //Assert.IsTrue(result.Contains("one two"));
        Console.WriteLine( result.Contains("three'four") ); //Assert.IsTrue(result.Contains("three'four"));
        Console.WriteLine( result.Contains("five") ); // Assert.IsTrue(result.Contains("five"));
    }
}

The output is

(?:Featuring|FEAT.|FEAT)\s*([^])]*)
3
True
True
True

Note how the regex pattern is built:

  • var keys = new List<string> { "FEAT", "FEAT.", "Featuring" }; initializes the keys string list with the search phrases
  • keys = keys.OrderByDescending(x => x.Length).ToList(); - sorts the items in the list by length in the descending order
  • var pattern = $@"(?:{string.Join("|", keys.Select(z => Regex.Escape(z)))})\s*([^])]*)"; - creates the regex pattern by putting the escaped search phrases into a non-capturing group with | alternation operator in between, (?:Featuring|FEAT\.|FEAT).
Related