0

Given a messageformat string such as str below. I want to be able to get the "notifications" & "name" values which are used to display the text values.

var str = @"You have {notifications, plural,
          zero {no notifications}
           one {one notification}
           =42 {a universal amount of notifications}
         other {# notifications}
        }. Have a nice day, {name}!";

I have tried using a regex such as:

var matches = Regex.Matches(str, @"{(.*?)}");
//var matches = Regex.Matches(str, @"(?<=\{)[^}{]*(?=\})");
var results = matches.Cast<Match>().Select(m => m.Groups[1].Value).Distinct().ToList();

But the above does not take in to account that {notifications,.. is itself wraped in curly braces and includes the inner values that are not needed, which are wrapped in curly braces also.

So in brief I just want to be able to parse a string such as str above and get notifications & name at the returned values.

A string such as var str2 = @"Hello {name}" should just return name as the value.

EDIT

The values notifications & name will not be known in advance - I have just used this as an example, for the values I require to return from the string.

egnomerator
  • 722
  • 6
  • 14
Paolo B
  • 2,206
  • 3
  • 16
  • 34

2 Answers2

1

TL;DR: Here is an optional solution

var str = @"You have {notifications, plural,
          zero {no notifications}
           one {one notification}
           =42 {a universal amount of notifications}
         other {# notifications}
        }. Have a nice day, {name}!";

// get matches skipping nested curly braces
var matches = 
    Regex.Matches(str, @"{((?:[^{}]|(?<counter>{)|(?<-counter>}))+(?(counter)(?!)))}");

var results = matches.Cast<Match>().Select(m => m.Groups[1].Value).Distinct()
    .Select(v => Regex.Match(v, @"^\w+").Value) // take 1st word
    .ToList();

which results in (copied from Visual Studio Locals window while debugging)

results Count = 2   System.Collections.Generic.List<string>
    [0] "notifications"
    [1] "name"

... original answer follows ...


One thing to note about the current solution in the original question:

  • the use of . doesn't match line breaks, so that's one reason why it currently matches the nested values (see this source)

If I understand your goal, this article is a nice explanation and demonstration of a related problem and solution:

(this article addresses the main challenge noted in the original question--the nested curly braces)

https://blogs.msdn.microsoft.com/timart/2013/05/14/nestedrecursive-regex-and-net-balancing-groups-detect-a-function-with-a-regex/

From that article, I'd suggest the below pattern as an optional solution:

var str = @"You have {notifications, plural,
          zero {no notifications}
           one {one notification}
           =42 {a universal amount of notifications}
         other {# notifications}
        }. Have a nice day, {name}!";

// get matches skipping nested curly braces
var matches = 
    Regex.Matches(str, @"{((?:[^{}]|(?<counter>{)|(?<-counter>}))+(?(counter)(?!)))}");
var results = matches.Cast<Match>().Select(m => m.Groups[1].Value).Distinct().ToList();

which results in (copied from Visual Studio Locals window while debugging)

results Count = 2   System.Collections.Generic.List<string>
    [0] "notifications, plural,\r\n          zero {no notifications}\r\n           one {one notification}\r\n           =42 {a universal amount of notifications}\r\n         other {# notifications}\r\n        "
    [1] "name"

(or if you were to print these results to the console):

// Result 0 would look like:
notifications, plural,
          zero {no notifications}
           one {one notification}
           =42 {a universal amount of notifications}
         other {# notifications}


// Result 1 would look like:
name

Update

I came back to this and realized that the question asked for just the single words as results.

Then take the first word from each result

(I'm repeating the above snippet with the additional select statement to show the full solution)

var str = @"You have {notifications, plural,
          zero {no notifications}
           one {one notification}
           =42 {a universal amount of notifications}
         other {# notifications}
        }. Have a nice day, {name}!";

// get matches skipping nested curly braces
var matches = 
    Regex.Matches(str, @"{((?:[^{}]|(?<counter>{)|(?<-counter>}))+(?(counter)(?!)))}");

var results = matches.Cast<Match>().Select(m => m.Groups[1].Value).Distinct()
    .Select(v => Regex.Match(v, @"^\w+").Value) // take 1st word
    .ToList();

which results in (copied from Visual Studio Locals window while debugging)

results Count = 2   System.Collections.Generic.List<string>
    [0] "notifications"
    [1] "name"

A little more information

(I just found this interesting and spent a little more time researching/learning and thought it worth including some more related information)

Conversations here and here include some opinions for and against using regex for this type of problem.

  • I think it's interesting to read these opinions and get a more well-rounded point of view

Regardless of the above opinions, .NET creators deemed it appropriate to implement balancing group definitions--a functionality this answer uses:

egnomerator
  • 722
  • 6
  • 14
0

One way to do this would be to write a method that will format a string for you based on an input count and the singular (and plural) form of the string:

private static string FormatWord(int count, string singluar)
{
    return Format(count, singluar, singluar + "s");
}

private static string FormatWord(int count, string singular, string plural)
{
    return count == 0 ? "no " + plural
        : count == 1 ? "one " + singular
        : count == 42 ? "a universal number of " + plural
        : count + " " + plural;
}

Then in use it might look like:

private static void Main()
{
    var name = "User";

    while (true)
    {
        var count = GetIntFromUser("Enter notification count: ");
        Console.WriteLine($"You have {FormatWord(count, "notification")}. " + 
            $"Have a nice day, {name}");
    }
}

Note that this method is also using a helper method to get a strongly-typed integer from the user:

private static int GetIntFromUser(string prompt, Func<int, bool> validator = null)
{
    int result;
    var cursorTop = Console.CursorTop;

    do
    {
        ClearSpecificLineAndWrite(cursorTop, prompt);
    } while (!int.TryParse(Console.ReadLine(), out result) ||
             !(validator?.Invoke(result) ?? true));

    return result;
}

private static void ClearSpecificLineAndWrite(int cursorTop, string message)
{
    Console.SetCursorPosition(0, cursorTop);
    Console.Write(new string(' ', Console.WindowWidth));
    Console.SetCursorPosition(0, cursorTop);
    Console.Write(message);
}
Rufus L
  • 32,853
  • 5
  • 25
  • 38