In einem aktuellen Software-Projekt haben wir uns mit einer interessanten Aufgabenstellung beschäftigt, die so oder in ähnlicher Form mit Sicherheit dem ein oder anderen bekannt vorkommen dürfte: Parsen von benutzerdefinierten Formeln nach einer deklarierten Syntax.
Die zu analysierenden Formeln waren wie folgt aufgebaut:
function1(param1,function2(param1,param2),param3)
Jede Funktion der Formel besteht aus einem Keyword (im Beispiel function1 und function2), das die eigentliche Funktion identifiziert. Eine durch Kommata getrennte Parameterliste der Funktion wird anschließend in einem Klammerausdruck angegeben. Die Funktionen können natürlich beliebig geschachtelt sein.
Die Aufgabenstellung bestand nun darin, die einzelnen Funktionen aus der Gesamtformel zu filtern um diese dann anschließend zu berechnen.
Reguläre Ausdrücke bieten sich an, ein solches Problem zu lösen.
.NET stellt im Namespace System.Text für die Arbeit mit regulären Ausdrücken die Klassen Regex zur Verfügung.
Eine erfolgreiche Strategie, um die geschachtelten Funktionen aufzulösen, besteht nun darin, nach beliebigen Funktionen zu suchen, die selbst keine anderen Funktionen mehr als Parameter besitzen (eine solche Funktion muss in der Formel existieren. Eine andere Strategie könnte z.B. zuerst die „innerste“ Funktion finden und berechnen).
Dazu lässt sich der folgende reguläre Ausdruck definieren, mit dem die gesamte Formel geparst wird:
Regex regex = new Regex("(function1|function2)[(][^(^)]*[)]");
Der erste Teil dieses regulären Ausdrucks definiert eine Liste mit Keywords von Funktionen, die bekannt sind und später berechnet werden sollen. Der zweite Teil sucht nach Klammerausdrücken, die selbst keine öffnenden und schließenden Klammern beinhalten.
Das folgende C#-Schnipsel zeigt, wie man solche Funktionen filtern kann:
using System.Text.RegularExpressions;
….
string s = "function1(param1,function2(param1,param2),param3)";
Regex regex = new Regex("(function1|function2)[(][^(^)]*[)]");
MatchCollection mc = regex.Matches(s);
for (int i=0; i < mc.Count; i++){
Console.WriteLine("Funktion an Pos.: " + mc[i].Index);
Console.WriteLine("Funktion: " + mc[i].Value);
computeValueOfFunction(mc[i].Value);
}
Im Beispiel liefert mc[0].Value z.B. function2(param1,param2).
Hat man erst einmal eine Funktion identifiziert, kann man diese nach der Ermittlung ihrer Parameter (die durch Kommata getrennt sind und zwischen den Klammern stehen) und des eigentlichen Funktions-Keywords (der linke Teil vor der öffnenden Klammer) berechnen (im Beispiel angedeutet durch computeValueOfFunction(). Anschließend wird diese Funktion in der Formel durch ihr Ergebnis ersetzt und die so modifizierte Formel erneut geparst. Dies wird solange wiederholt, bis alle Funktionen berechnet wurden und die verbleibende Formel-Zeichenkette das Gesamtergebnis des Ausdrucks darstellt.