My Regex Function Parser

AND THEY SAID IT COULDN’T BE DONE!… or was that “shouldn’t”?
Oh well, in any case, here is a FunctionParser I made to parse text functions.
For example, this:


var MyText = "Here is my text. It has @replace(""some text in which I’d like to replace this"",""this"",""withthat""). I'd also like to know what time it is @now(""MMM dd, yyyy"") and what it would look like if i formatted a date like this @formatdate(""4/16/2010"",""yyyyMMdd"")";

Console.Write(FunctionParser.Process(MyText));

would output:

“Here is my text. It has some text in which I’d like to replace withthat. I’d also like to know what time it is Oct 26, 2010 and what it would look like if i formatted a date like this 20100416”

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.UI;

namespace MyCompany.Core.Text
{
    /// <summary>
    /// This class is used to parse function in a text string. It handles replacing the
    /// function placeholders such as, @replace("sometex","x","xt), with the function result.
    /// </summary>
    public static class FunctionParser
    {
        private const string _MatchingParentheses = @"\((?>[^()]+|\((?<number>)|\)(?<-number>))*(?(number)(?!))\)";
        private const string _NestedFunctions = @"@(?<function>\w+)\(((?<arg>\""[^\""]*\""|\d+|(?<nestedfunction>@\w+" + _MatchingParentheses + @"))(?:,?))+\)";
        private static Regex _rxFunction = new Regex(_NestedFunctions, RegexOptions.Compiled | RegexOptions.Singleline);
        private static Regex _rxBooleanExpression = new Regex(@"(?<operand1>[^=<>!]+)\s*(?<operator>[=<>!]+)\s*(?<operand2>[^=<>!]+)", RegexOptions.Compiled | RegexOptions.Singleline);
        private static Regex _rxVariable = new Regex(@"@\{[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}\}", RegexOptions.Compiled | RegexOptions.Singleline);

        public static string Process(string text)
        {
            return Process(text, null, null);
        }

        public static string Process(string text, CustomFunctionHandler fxHandler, StringDictionary variables)
        {
            string result = string.Empty;
            try
            {
                result = Process(text, fxHandler, 0, variables);
            }
            catch (Exception ex)
            {
                Log.Error("FunctionParser error. Input: " + text, ex);
                result = "FunctionParser error: " + ex.Message;
            }
            return result;
        }

        private static string Process(string text, CustomFunctionHandler fxHandler, int stack, StringDictionary variables)
        {
            stack++;
            if (stack >= 10)
            {
                throw new Exception("FunctionParser max recursion level (10) exceeded!");
            }

            string result = _rxFunction.Replace(text, delegate(Match m)
            {
                // Preset the return value
                string replacewith = "'@" + m.Groups["function"].Value + "' function is not recognized.";
                string[] args = m.Groups["arg"].Captures
                                    .Cast<Capture>()
                                    .Select<Capture, string>(x => x.Value.TrimStart(new char[] { '"' }).TrimEnd(new char[] { '"' }))
                                    .ToArray();

                // Resolve variables...
                if (variables != null && variables.Count > 0)
                {
                    // replace variables in each arg with the value
                    args = args.Select(delegate(string arg)
                            {
                                return _rxVariable.Replace(arg, x => variables.ContainsKey(x.Value) ? variables[x.Value] : string.Empty);
                            })
                            .ToArray();
                }

                // Process nested functions first...
                List<string> nestedfunctions = m.Groups["nestedfunction"].Captures
                                                    .Cast<Capture>()
                                                    .Select<Capture, string>(x => x.Value)
                                                    .ToList();
                if (nestedfunctions.Count > 0)
                {
                    for (int i = 0; i < args.Length; i++)
                    {
                        if (nestedfunctions.Contains(args[i]))
                            args[i] = Process(args[i], fxHandler, stack, variables);
                    }
                }

                switch (m.Groups["function"].Value)
                {
                    case "replace":
                        replacewith = args[0].Replace(args[1], args[2]);
                        break;
                    case "formatdate":
                        replacewith = FormatDate(args[0], args[1]);
                        break;
                    case "now":
                        replacewith = DateTime.Now.ToString(args[0]);
                        break;
                    case "if":
                        if (EvaluateBooleanExpression(args[0]))
                            replacewith = args[1];
                        else
                            replacewith = args[2];
                        break;
                    case "dateadd":
                        replacewith = DateAdd(args[0], args[1]);
                        break;
                    case "substring":
                        int startIndex = int.Parse(args[1]);
                        if (args.Length == 2)
                            replacewith = args[0].Substring(startIndex);
                        else if (args.Length == 3)
                        {
                            int length = int.Parse(args[2]);
                            replacewith = args[0].Substring(startIndex, Math.Min(args[0].Length - startIndex, length));
                        }
                        break;
                    default:
                        if (fxHandler != null)
                            replacewith = fxHandler(m.Groups["function"].Value, args);
                        break;
                }

                return replacewith;
            });

            return result;
        }

        public static bool EvaluateBooleanExpression(string expr)
        {
            bool result = false;

            Match m = _rxBooleanExpression.Match(expr);
            if (m.Success)
            {
                string op1 = m.Groups["operand1"].Value.Trim();
                string op2 = m.Groups["operand2"].Value.Trim();

                switch (m.Groups["operator"].Value)
                {
                    case "=":
                    case "==":
                        result = op1 == op2;
                        break;
                    case "!=":
                        result = op1 != op2;
                        break;
                }
            }

            return result;
        }

        private static string FormatDate(string datestring, string formatstring)
        {
            string result = string.Empty;
            DateTime date;

            if (DateTime.TryParse(datestring, out date))
                result = date.ToString(formatstring);

            return result;
        }

        private static string DateAdd(string timespan, string datestring)
        {
            string result = string.Empty;
            DateTime date;
            TimeSpan ts;

            if (DateTime.TryParse(datestring, out date))
            {
                if (TimeSpan.TryParse(timespan, out ts))
                    result = date.Add(ts).ToString();

            }

            return result;
        }
    }
}

The CustomFunctionDelegate allows you to pass in the implementation for your own custom functions (although you could just as easily modify this class to add it in).

The “variables” dictionary is used in the case you were passing in your function parameters as variables instead of inline text. So instead of:

@replace(“some text in which I’d like to replace this”,”this”,”withthat”)

this:

@replace(“@MyVar1″,”@MyVar2″,”@MyVar3”)

and the string dictionary:

@MyVar1 = “some text in which I’d like to replace this”

@MyVar2 = “this”

@MyVar3 = “withthat”

There you have it. Use it at your own risk. 🙂

Advertisements

About Paul Martin

I enjoy rock climbing, playing guitar, writing code...
This entry was posted in .NET, C#, regex. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s