|
» myCSharp.de Diskussionsforum |
|
|
|
Autor
 |
|
Th69
myCSharp.de-Poweruser/ Experte
Dabei seit: 01.04.2008
Beiträge: 3.851
Entwicklungsumgebung: Visual Studio 2015/17
|
|
Parser für mathematische Formeln:
Nachdem ich in letzter Zeit in diversen Forumsbeiträgen Anfragen zu Parsern für mathematische Formeln gesehen habe, habe ich mich an meinen (vor ca. 10 Jahren) in C++ geschriebenen Parser erinnert und mir gedacht, den könnte ich doch mal nach C# umsetzen.
Mir geht es insbesondere darum, auch ein bißchen die Theorie verständlich zu machen. Da oftmals als Ansätze die Stringersetzung (egal ob per Hand oder per Regex) gewählt wurde, welche jedoch im Detail dann oftmals Probleme mit den Operatorprioritäten sowie Klammererkennung haben, möchte ich meinen generischen Parser vorstellen, welcher auch nur aus ca. 200 Zeilen besteht.
Ich weiß, daß man in C# bzw. generell mit allen .NET-Sprachen auch per CodeDOM sich den Ausdrucksinterpreter zunutze machen kann.
Mein Parser kann jedoch als Vorlage für unterschiedliche Bereiche benutzt werden, da er flexibel angepaßt werden kann.
1. Theorie
Nach der Einteilung von Chomsky ( http://de.wikipedia.org/wiki/Chomsky-Hierarchie) ist eine mathematische Formel eine kontextfreie Sprache (bzw. Grammatik). Und diese lassen sich formal mithilfe der (E)BNF-Notation ( http://de.wikipedia.org/wiki/EBNF) beschreiben.
Code: |
1:
2:
3:
4:
5:
6:
|
Expression = Term | Term ('+' | '-') Expression
Term = Factor | Factor ('*' | '/') Term
Factor = Terminal | Terminal '^' Factor
Terminal = ['+' | '-'] (Number | Variable | Constant | Function | '(' Expression ')')
Function = identifier '(' Expression ')' |
|
"Expression" stellt hierbei der Einstiegspunkt für die mathematischen Formeln dar. Die einzelnen Operatorprioritäten sind mittels der verschiedenen Hierarchie-Ebenen abgebildet, d.h. das binäre Plus (+) sowie das Minus (-) bilden die schwächste Priorität, während die Potenz '^' die höchste bildet. Innerhalb einer Priorität werden hierbei die Operatoren von links nach rechts abgearbeitet.
Auf der untersten Ebene werden die Terminalsymbole sowie unären Operatoren Plus und Minus als auch die Klammerausdrücke behandelt. Durch den rekursiven Aufruf von 'Expression' auf unterster Ebene können somit alle Formeln beliebiger Komplexität erzeugt werden.
Nun können aus der EBNF-Definition relativ einfach die Parsermethoden erzeugt werden. Benötigt wird hierzu eine Hilfsfunktion 'GetChar', welche das jeweils nächste Zeichen 'parsed', wobei Leerzeichen (Whitespaces) überlesen werden.
C#-Code: |
protected char GetChar()
{
char c;
do
{
c = GetNextChar();
}
while(c != '\0' && Char.IsWhiteSpace(c));
return c;
}
protected char GetNextChar()
{
return Functionator.MoveNext()? Functionator.Current : '\0';
}
IEnumerator<char> Functionator;
|
Functionator ist dabei ein Enumerator auf die zu parsende Formel (extra als IEnumerator<char> definiert - statt string - um möglichst allgemeingültig zu sein).
Das jeweils nächste zu parsende Zeichen wird per Referenz (ref char c) an die einzelnen Parsermethoden übergeben, so daß nach dem Aufruf in der Variable 'c' das nächste zu parsende Zeichen drin steht.
Dann sieht eine Parsermethode z.B. so aus (Datentyp sei jetzt 'double'):
C#-Code: |
double Expression(ref char c)
{
double y = Term(ref c);
while(c == '+' || c == '-')
{
switch(c)
{
case '+': c = GetChar(); y += Term(ref c); break;
case '-': c = GetChar(); y -= Term(ref c); break;
}
}
return y;
}
|
Analog würde man die anderen Parsermethoden definieren...
2. MathParser
Bei meinem Parser wollte ich jedoch auch die Operatoren frei definieren können. Somit brauchte ich eine Methode, welche allgemeingültig für alle Operatorprioritäten funktioniert.
Schematisch sieht die Funktionalität dann so aus:
Code: |
1:
2:
3:
|
Calculate(pri) = Calculate(pri+1) |
Calculate(pri+1) Operator(pri) Calculate(pri)
Calculate(MaxPri) = Terminal |
|
Die Priorität 'pri' ist dann von 1..MaxPri definiert.
Die Methode 'Calculate' habe ich dann folgendermaßen umgesetzt (T ist der verwendete Datentyp, d.h. der generische Parameter der Klasse):
C#-Code: |
T Calculate(ref char c, OpPriority op_pri)
{
T y = Calc(ref c, op_pri);
Operator op;
while((op = GetOperator(c, OpType.Binary, op_pri)) != null)
{
T y2 = Calc(ref c, op_pri);
y = op.op(y, y2);
}
return y;
}
T Calc(ref char c, OpPriority op_pri)
{
T y;
if(op_pri < MaxOpPriority)
y = Calculate(ref c, (OpPriority)(op_pri + 1));
else
y = GetTerminal(ref c);
return y;
}
|
Die Methode 'Calc' ist eine Hilfsmethode, da diese zweimal innerhalb von Calculate aufgerufen wird. Je nach Operatorpriorität wird entweder rekursiv die Methode 'Calculate' mit der nächsthöheren Priorität aufgerufen oder aber der Wert eines Terminalsymbol bestimmt.
Damit der Parser flexibel angepaßt werden kann, benötigt er diverse Strukturen (bzw. Klassen) für die einzelnen Terminalsymbole:
C#-Code: |
using OpPriority = System.SByte;
protected delegate T Fct(T x);
protected class Function
{
public string sFct;
public Fct fct;
};
protected delegate T Op(T x, T y);
protected enum OpType
{
Unary, Binary
}
protected class Operator
{
public string sOp;
public Op op;
public OpType type;
public OpPriority priority;
};
protected class Constant
{
public string sConst;
public T value;
};
|
Zu jeder dieser Klassen gibt es eine Membervariable, welche eine Aufzählung beinhaltet:
C#-Code: |
protected IEnumerable<Function> Functions;
protected IEnumerable<Operator> Operators;
protected IEnumerable<Constant> Constants;
|
Die komplexeste Methode stellt 'GetTerminal' dar, welche aufgrund des aktuellen Zeichens entscheidet, welches Terminalsymbol ausgewertet wird:
- optional unärer Operator ('+' oder '-')
- öffnende Klammer '(': rekursiver Aufruf von Expression
- Ziffer oder Punkt: Zahl auswerten (GetNumber)
- Buchstabe: Variable, Konstante oder Funktion
- bei Funktion noch Klammern sowie Parameter (Expression) auswerten
Da die Parser-Klasse selber generisch bzgl. des verwendeten Datentyps ist, lassen sich mittels Ableitung von dieser Klasse diverse Parser entwickeln.
C#-Code: |
public class FctParser<T>
{
public T Parse(IEnumerable<char> function, T x);
};
public class FormulaParser : FctParser<double>
{
public FormulaParser()
{
base.Functions = Functions;
base.Operators = Operators;
base.Constants = Constants;
base.MaxOpPriority = 3;
}
static new Function[] Functions =
{
new Function("abs", Math.Abs),
new Function("acos", Math.Acos),
}
static double Add(double x, double y)
{
return x + y;
}
static new Operator[] Operators =
{
new Operator('+', Add, OpType.Unary, 0),
new Operator('-', Sub, OpType.Unary, 0),
new Operator('^', Math.Pow, OpType.Binary, 3),
new Operator('#', Root, OpType.Binary, 3),
new Operator('*', Mul, OpType.Binary, 2),
new Operator('/', Div, OpType.Binary, 2),
new Operator('%', Mod, OpType.Binary, 2),
new Operator('+', Add, OpType.Binary, 1),
new Operator('-', Sub, OpType.Binary, 1)
};
static new Constant[] Constants =
{
new Constant("E", Math.E),
new Constant("PI", Math.PI)
};
};
|
Der Aufruf des Formelparsers ist dann ganz einfach:
C#-Code: |
FormulaParser fp = new FormulaParser();
try
{
string sFct = "14 * 3^2 * (sin(x)^2 + cos(x)^2) / (30.0e-1 * ln(E))";
double x = 10.42;
double y = fp.Parse(sFct, x);
}
catch(FctException ex)
{
MessageBox.Show(ex.Message, "Error");
}
|
Sollte die Formel nicht korrekt sein, so wird eine 'FctException' geworfen.
Analog lassen sich somit auch Parser (bzw. Interpreter) auf der Grundlage anderer Datentypen (z.B. decimal, int, long, Complex, ...) erstellen.
Im angehängten Projekt ist außerdem noch ein Beispiel für einen Parser basierend auf logischen Ausdrücken (LogicParser).
Desweiteren ist dort ein Testprojekt für den FormulaParser: FctParser.
Außerdem verfügt der Parser noch über einige weitere Einstellungsmöglichkeiten:
- Culture: verwendete CulturInfo für die Zahlen (Standard: invariant, d.h. Punkt '.' als Trennzeichen bei "Kommazahlen" -)
- IgnoreCase: gibt an, ob die Groß-/Kleinschreibung ignoriert wird (Standard: true)
- FunctionBracket, OperatorBracket: Struktur für die öffnende und schließende Klammer (Standard: jeweils '(' und ')' )
- ParseNumber: virtuelle Methode zum Überschreiben, falls nicht die Standardzahlen (ParseDouble) verwendet werden
Edit:
- IsNumberChar: virtuelle Methode, um Zahlen zu erkennen (Standard: Ziffern und Komma-Trennzeichen)
- IsIdentifierChar: virtuelle Methode, um Namen für Variablen, Funktionen und Konstanten zu erkennen (Standard: nur Buchstaben)
- Operatoren aus mehreren Zeichen (z.B. &&, <=, !=, etc.)
Desweiteren habe ich zwei weitere Beispielparser in das Projekt reingepackt:
- ConditionalParser<T, Parser>: Erweiterung eines Parsers (z.B. FormulaParser) um Vergleichsoperatoren
- IntParser: Ganzzahlarithmetik sowie Bitoperationen
Edit #2:
Ich habe nochmals das Projekt erweitert und den Punkt
- Aufbau eines Abstract Syntax Trees (AST), d.h. "Vorkompilierung"
umgesetzt.
Es gibt jetzt zwei weitere Klassen:
- Eval
- ExprTree
Durch die Vorkompilierung habe ich Performanceverbesserungen bei der Auswertung von ca. 20 - 60 mal hinbekommen.
Die Anwendung ist ebenso einfach wie zuvor:
C#-Code: |
Eval<double> eval = new Eval<double>();
FormulaParser fp = new FormulaParser();
string sFct = "2*x+PI";
eval.Parse(fp, sFct);
string sOptFct = eval.ToString();
for(double x = 0; x < 1000; x++)
{
double y = eval.Evaluate(x);
}
|
Die Vorberechnung lohnt sich natürlich nur dann, wenn man dieselbe Formel für verschiedene Parameterwerte (x) aufruft.
Um die verschiedenene Formeln besser zu testen, habe ich auch das Textprojekt "FctParser" erweitert. Man kann nun zwischen 6 verschiedenen Parsern wählen und entweder direkt die Formel auswerten oder aber mittels der "StopWatch" Performance-Messungen durchführen. Die CheckBox 'Evaluate' aktiviert dann den Aufbau des ExpressionTrees mittels der Klasse Eval.
Die Basis-Klasse 'FctParser<T>' habe ich um die Interface-Klassen 'IParser<T>' sowie die nicht-generische Klasse 'IParser' erweitert.
Desweiteren gibt es nun 5 virtuelle Execute...-Methoden, welche die Eval-Klasse zum Aubau des ExprTree benötigt (anstatt direkt das Ergebnis auszurechnen).
Als Bonbon kann man sich im Testprojekt nun mit dem Info-Button "?" die verwendeten Funktionen, Operatoren, Konstanten sowie Variablen anzeigen lassen (per MouseHover bzw. bei Klick als MessageBox-Dialog).
Edit #3:
So als vorerst letzte Erweiterung habe ich nun den Punkt
- Funktionen mit mehr als einem Parameter
umgesetzt.
Es gibt nun die zusätzliche generische Klasse FctCaller, welche Delegates mit einer unterschiedlichen Anzahl von Parametern erlaubt. Implementiert habe ich bisher die davon abgeleiteten Klassen für 1, 2 und beliebig viele (-1) Parameter. Als zusätzliche Beispiele dafür habe ich im FormulaParser die Methoden 'max', 'min' sowie 'sum' hinzugefügt.
---------------------------------------------------------------------
So ich hoffe, dieser Parser finden Anklang und Verwendung.
Es gibt sicherlich auch noch einige mögliche Verbesserungen:
- mehrere Variablen (anstatt nur 'x') (wobei dies auch über die Konstantenliste erreicht werden kann)
- Performanceverbesserungen beim Lookup (Dictionary statt List)
- ...
Bei explizitem Interesse kann ich ja auch noch diese einzelnen Punkte umsetzen...
Schlagwörter: Parser, FormelParser, MathParser
Dieser Beitrag wurde 7 mal editiert, zum letzten Mal von Th69 am 06.10.2011 12:01.
|
|
30.05.2009 11:44
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hi, Th69.
Coole Sache!
Mit einer kleinen Anpassung ist ein Hex-Rechner auch machbar.
Mit diesem kann man dann z.B. folgendes rechnen: 0xF0 | 0x0F ergibt 255 [FF]
Zahlen können im dezimalen oder im hexadezimalen (mit Präfix 0x) System eingegeben werden.
Operatoren: And ( &), Or ( |), Xor ( ^), Not ( ~).
In der Klasse FctParser<T> in der Methode T GetTerminal(ref char c) folgendes ändern:
C#-Code: |
if (c == OperatorBracket.bra)
{ ... }
else if (IsDigit(c))
...
else if (Char.IsLetter(c))
{ ... }
|
Und die Methode bool IsDigit(char c) hinzufügen:
C#-Code: |
protected virtual bool IsDigit(char c)
{
return Char.IsDigit(c) || c == Dot;
}
|
Damit sind die Grundlagen für folgenden Hex-Rechner gelegt:
C#-Code: |
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace MathParser
{
public class HexCalculator : FctParser<Int32>
{
public HexCalculator()
{
base.Functions = Functions;
base.Operators = Operators;
base.Constants = Constants;
base.MaxOpPriority = 3;
}
static new Function[] Functions =
{
};
static Int32 Not(Int32 x, Int32 y)
{
return ~y;
}
static Int32 Or(Int32 x, Int32 y)
{
return x | y;
}
static Int32 And(Int32 x, Int32 y)
{
return x & y;
}
static Int32 Xor(Int32 x, Int32 y)
{
return x ^ y;
}
static new Operator[] Operators =
{
new Operator('~', Not, OpType.Unary, 0),
new Operator('^', Xor, OpType.Binary, 3),
new Operator('|', Or, OpType.Binary, 2),
new Operator('&', And, OpType.Binary, 1),
};
static new Constant[] Constants =
{
};
private static List<Char> m_HexChars = new List<Char>("0123456789abcdefABCDEFxX".ToCharArray());
protected override Boolean IsDigit(Char c)
{
return m_HexChars.Contains(c);
}
private static Regex m_HexRegex = new Regex(@"0x(?<Digits>[a-f0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
protected override Int32 ParseNumber(ref Char c)
{
StringBuilder sb = new StringBuilder();
while (IsDigit(c))
{
sb.Append(c);
c = GetNextChar();
}
var str = sb.ToString();
var match = m_HexRegex.Match(str);
if (match.Success)
{
return Convert.ToInt32(match.Groups["Digits"].Value, 16);
}
return Convert.ToInt32(str);
}
}
}
|
Gruß, Christian.
Dieser Beitrag wurde 2 mal editiert, zum letzten Mal von TheBrainiac am 30.05.2009 17:08.
|
|
30.05.2009 17:03
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hallo Christian,
ja du hast recht, die Methode 'IsDigit' muß eingebaut werden, damit sie synchron zu 'ParseNumber' ist. Danke für deinen Hinweis.
Und ich finde es toll, daß mein Parser mit wenig Aufwand so flexibel ist - ich selber hatte gar nicht an eine Hex-Berechnung gedacht -)
Vielleicht finden sich ja noch mehr Parser-Derivate...
P.S. Da ich jetzt nicht an meinem Entwicklungsrechner sitze, werde ich morgen dann das Projekt aktualisieren... (wofür ist Pfingsten sonst da -)
Edit: Ich habe das Projekt aktualisiert und neue virtuelle Methoden hinzugefügt:
- IsNumberChar (aussagekräftigerer Name statt 'IsDigit')
- IsIdentifierChar (analog zu IsNumberChar, nur daß damit Namen für Variablen, Funktionen und Konstanten erkannt werden)
s.a. Edit-Bereich im Hauptbeitrag
Edit #2: Weitere Verbesserungen:
- Operatoren aus mehreren Zeichen, d.h. String statt nur ein Char
- 2 weitere Beispielparser: ConditionalParser<T, Parser> sowie IntParser
Dieser Beitrag wurde 3 mal editiert, zum letzten Mal von Th69 am 08.06.2009 15:58.
|
|
31.05.2009 10:04
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Ich habe den Formelparser noch mal (mit einigem Aufwand) erweitert (s. Edit #2 im Hauptbeitrag).
Zum Einsatz dieses Parsers in z.B. Graphprogrammen kann die neue Klasse 'Eval' nun hervorragend eingesetzt werden, um deutliche Performanceverbesserungen zu erzielen: einfach mal selber im Testprojekt ausprobieren...
Die vorherige Version habe ich jetzt hier in diesen Anhang verschoben (insbesondere für Anfänger, denen die aktuelle Version evtl. zu komplex ist).
|
|
13.06.2009 13:39
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
ikaros
myCSharp.de-Mitglied
Dabei seit: 27.05.2005
Beiträge: 1.739
|
|
@Th69: Mir gefällt die Idee, es ist benutzbar. Weiter so.
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von ikaros am 15.06.2009 20:54.
|
|
15.06.2009 20:54
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Als nun vorerst letzte Erweiterung habe ich Funktionen mit einer unterschiedlichen Anzahl von Parametern implementiert (s. Edit #3 im Hauptbeitrag).
Ich wollte mich auch noch für die 18 Leute bedanken, die bisher meinen Parser heruntergeladen haben (leider wird der Counter bei jeder neuen Version wieder zurückgesetzt -). Also auf ein neues...
|
|
07.07.2009 13:45
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Als Anwendungsprogramm für den MathParser habe ich mal gerade ein Programm zur Anzeige eines Funktionsgraphen entwickelt (ist sicherlich noch erweiterungsfähig, aber damit kann man schnell mal die Funktionen testen).
Edit: ein paar kleine Fehler beseitigt, kosmetische Korrekturen vorgenommen sowie Performance-Verbesserungen (z.B. bei "log(x)")...
Dateianhang: |
Graph.zip (54,48 KB, 1.310 mal heruntergeladen) |
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Th69 am 15.07.2009 09:59.
|
|
14.07.2009 16:57
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegt mehr als ein Monat. |
Zentauro
myCSharp.de-Mitglied
Dabei seit: 06.04.2007
Beiträge: 112
Entwicklungsumgebung: VS 2012 RC Herkunft: Linz/Oberösterreich
|
|
echt geniales ding - DANKE für deine mühen!!!
|
|
20.08.2009 09:13
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 2 Monate. |
Cat
myCSharp.de-Mitglied
Dabei seit: 25.10.2009
Beiträge: 771
|
|
Super, genau so eine Komponente habe ich gesucht.
Besonders schön, daß man die Funktionen und Operatoren so einfach anpassen kann - vielen Dank dafür.
|
|
25.10.2009 10:07
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 3 Monate. |
Torgrimm
myCSharp.de-Mitglied
Dabei seit: 10.05.2007
Beiträge: 24
Entwicklungsumgebung: VS2010/12 Prem;Office 2.0-2010 Herkunft: Aschaffenburg
|
|
Hallo Th69,
erstmal - ein wunderbares Tool hast du da erstellt. Auch die Erklärungen dazu sind schön verständlich und wecken wohlige Erinnerungen ans Studium.
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Torgrimm am 26.01.2010 14:08.
|
|
26.01.2010 13:49
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Torgrimm
myCSharp.de-Mitglied
Dabei seit: 10.05.2007
Beiträge: 24
Entwicklungsumgebung: VS2010/12 Prem;Office 2.0-2010 Herkunft: Aschaffenburg
|
|
Hallo,
ich habe mich mal daran gemacht und den offenen Punkt umgesetzt, mehrere Variablen verwenden zu können. Gelöst ist es wie von dir angeregt über ein Konstanten Dictionary. Im Prinzip funktioniert das auch genauso mit den Variablen selbst.
Außerdem ist es nun möglich Konstanten (oder apäter auch Variablen) in einer Formel mit dem '=' Operator zu setzen. Für meinen Anwendungsfall missbrauche ich den Parser ein wenig und arbeite mit Strings anstatt von Zahlen. Daher gibt es zwei neue Reservierte Zeichen, das '@' und das ".
Das '@' schließt eine Konstante ein, die auf ihren Wert aufgelöst werden soll (R-Value).
Das " schließt eine Konstante ein, deren Wert neu zugewiesen werden soll(L-Value) oder aber einen beliebigen String für eine Operation, zB. einen Vergleich (R-Value);
Beide Zeichen sind statisch in der Constant-Klasse belegbar. Die beiden Zeichen fügen sich in der Priorität noch vor den Klammern ein.
Soweit so schön. Dummerweise hat sich hier noch ein Bug eingeschlichen dem ich gerade etwas ratlos gegenüber stehe. Jedenfalls kann ich mir das beobachtete Verhalten nicht richtig erklären. Ihc habe dazu eine Beispielanwendung beigefügt, die den Fehler demonstriert.
Was ist eigentlich los?
Nun, die erste Instanz des Parsers funktioniert ganz tadellos.
Wird diese nun allerdings Zerstört und danach eine neue Instanz erzeugt, scheint die neue in einem bestimmten Kontext noch auf Constants der ersten Instanz zuzugreifen. Ich gehe davon aus, das in der Vererbung irgendwo der Bock steckt. Ich habe jedoch bereits mit allen möglichen Kombinationen von this, base und virtual rumgespielt - jedoch ohne positives Ergebnis.
C#-Code: |
class Program
{
static void Main(string[] args)
{
Test();
Debug.Print("\n\n\n\nZweiter Durchlauf:");
Test();
}
private static void Test()
{
Dictionary<string, string> constantsIn = new Dictionary<string, string>();
constantsIn.Add("a", "0");
constantsIn.Add("b", "0");
constantsIn.Add("c", "0");
constantsIn.Add("d", "0");
constantsIn.Add("e", "0");
constantsIn.Add("f", "0");
constantsIn.Add("g", "0");
constantsIn.Add("h", "0");
constantsIn.Add("i", "0");
List<string> rules = new List<string>();
rules.Add("\"a\" = \"1\"");
rules.Add("\"b\" = \"2\"");
rules.Add("\"c\" = \"3\"");
rules.Add("\"d\" = \"4\"");
rules.Add("\"e\" = \"5\"");
rules.Add("\"f\" = \"6\"");
rules.Add("\"g\" = \"7\"");
rules.Add("\"h\" = \"8\"");
rules.Add("\"i\" = \"9\"");
ConditionalAssignmentParser<String, FctParser<Object>, Object> Parser = new ConditionalAssignmentParser<String, FctParser<Object>, Object>();
foreach (string s in constantsIn.Keys)
{
try
{
Parser.AddConstant(s, constantsIn[s]);
}
catch (Exception ex)
{
Debug.Print("Die Konstante " + s + " konnte dem Parser nicht hinzugefügt werden.", ex);
}
}
foreach (string rule in rules)
{
try
{
Parser.Parse(rule);
}
catch (Exception ex)
{
Debug.Print("Fehler beim Ausführen einer Regel!", ex);
}
}
foreach (FctParser<String>.Constant result in Parser.Constants.Values)
{
Debug.Print(result.sConst + ": " + result.value);
}
}
}
|
Die Methode Test() zweifach aufgerufen führt zu der folgenden Ausgabe:
Create FctParser: MathParser.ConditionalAssignmentParser`3 [System.String,MathParser.FctParser`1 [System.Object],System.Object] Hash: 45653674
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.String]] Hash: 41149443
Create ConditionalParser: MathParser.ConditionalAssignmentParser`3 [System.String,MathParser.FctParser`1 [System.Object],System.Object] Hash: 45653674
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.String]] Hash: 41149443
Create FctParser: MathParser.FctParser`1 [System.Object] Hash: 39785641
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.Object]] Hash: 45523402
Create ConditionalAssignmentParser: MathParser.ConditionalAssignmentParser`3 [System.String,MathParser.FctParser`1 [System.Object],System.Object] Hash: 45653674
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.String]] Hash: 41149443
Calculate Vorher a: 00 Constant Hash:41149443
Assign Vorher a: 000 Constant Hash:41149443
Assign sId Hash: -842352705 Constant Hash:41149443
Assign Nachher a: 111 Constant Hash:41149443
Calculate Nachher a: 11 Constant Hash:41149443
Calculate Hash in Calculate a: -842352705 Constant Hash:41149443
Calculate Vorher b: 00 Constant Hash:41149443
Assign Vorher b: 000 Constant Hash:41149443
Assign sId Hash: -842352706 Constant Hash:41149443
Assign Nachher b: 222 Constant Hash:41149443
Calculate Nachher b: 22 Constant Hash:41149443
Calculate Hash in Calculate b: -842352706 Constant Hash:41149443
Calculate Vorher c: 00 Constant Hash:41149443
Assign Vorher c: 000 Constant Hash:41149443
Assign sId Hash: -842352707 Constant Hash:41149443
Assign Nachher c: 333 Constant Hash:41149443
Calculate Nachher c: 33 Constant Hash:41149443
Calculate Hash in Calculate c: -842352707 Constant Hash:41149443
Calculate Vorher d: 00 Constant Hash:41149443
Assign Vorher d: 000 Constant Hash:41149443
Assign sId Hash: -842352708 Constant Hash:41149443
Assign Nachher d: 444 Constant Hash:41149443
Calculate Nachher d: 44 Constant Hash:41149443
Calculate Hash in Calculate d: -842352708 Constant Hash:41149443
Calculate Vorher e: 00 Constant Hash:41149443
Assign Vorher e: 000 Constant Hash:41149443
Assign sId Hash: -842352709 Constant Hash:41149443
Assign Nachher e: 555 Constant Hash:41149443
Calculate Nachher e: 55 Constant Hash:41149443
Calculate Hash in Calculate e: -842352709 Constant Hash:41149443
Calculate Vorher f: 00 Constant Hash:41149443
Assign Vorher f: 000 Constant Hash:41149443
Assign sId Hash: -842352710 Constant Hash:41149443
Assign Nachher f: 666 Constant Hash:41149443
Calculate Nachher f: 66 Constant Hash:41149443
Calculate Hash in Calculate f: -842352710 Constant Hash:41149443
Calculate Vorher g: 00 Constant Hash:41149443
Assign Vorher g: 000 Constant Hash:41149443
Assign sId Hash: -842352711 Constant Hash:41149443
Assign Nachher g: 777 Constant Hash:41149443
Calculate Nachher g: 77 Constant Hash:41149443
Calculate Hash in Calculate g: -842352711 Constant Hash:41149443
Calculate Vorher h: 00 Constant Hash:41149443
Assign Vorher h: 000 Constant Hash:41149443
Assign sId Hash: -842352696 Constant Hash:41149443
Assign Nachher h: 888 Constant Hash:41149443
Calculate Nachher h: 88 Constant Hash:41149443
Calculate Hash in Calculate h: -842352696 Constant Hash:41149443
Calculate Vorher i: 00 Constant Hash:41149443
Assign Vorher i: 000 Constant Hash:41149443
Assign sId Hash: -842352697 Constant Hash:41149443
Assign Nachher i: 999 Constant Hash:41149443
Calculate Nachher i: 99 Constant Hash:41149443
Calculate Hash in Calculate i: -842352697 Constant Hash:41149443
T: 1
F: 0
true: 1
false: 0
a: 1
b: 2
c: 3
d: 4
e: 5
f: 6
g: 7
h: 8
i: 9
Zweiter Durchlauf:
Create FctParser: MathParser.ConditionalAssignmentParser`3 [System.String,MathParser.FctParser`1 [System.Object],System.Object] Hash: 35287174
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.String]] Hash: 44419000
Create ConditionalParser: MathParser.ConditionalAssignmentParser`3 [System.String,MathParser.FctParser`1 [System.Object],System.Object] Hash: 35287174
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.String]] Hash: 44419000
Create FctParser: MathParser.FctParser`1 [System.Object] Hash: 52697953
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.Object]] Hash: 22597652
Create ConditionalAssignmentParser: MathParser.ConditionalAssignmentParser`3 [System.String,MathParser.FctParser`1 [System.Object],System.Object] Hash: 35287174
Constants: System.Collections.Generic.Dictionary`2 [System.String,MathParser.FctParser`1+Constant [System.String]] Hash: 44419000
Calculate Vorher a: 00 Constant Hash:44419000
Assign Vorher a: 111 Constant Hash:41149443
Assign sId Hash: -842352705 Constant Hash:41149443
Assign Nachher a: 111 Constant Hash:41149443
Calculate Nachher a: 00 Constant Hash:44419000
Calculate Hash in Calculate a: -842352705 Constant Hash:44419000
Calculate Vorher b: 00 Constant Hash:44419000
Assign Vorher b: 222 Constant Hash:41149443
Assign sId Hash: -842352706 Constant Hash:41149443
Assign Nachher b: 222 Constant Hash:41149443
Calculate Nachher b: 00 Constant Hash:44419000
Calculate Hash in Calculate b: -842352706 Constant Hash:44419000
Calculate Vorher c: 00 Constant Hash:44419000
Assign Vorher c: 333 Constant Hash:41149443
Assign sId Hash: -842352707 Constant Hash:41149443
Assign Nachher c: 333 Constant Hash:41149443
Calculate Nachher c: 00 Constant Hash:44419000
Calculate Hash in Calculate c: -842352707 Constant Hash:44419000
Calculate Vorher d: 00 Constant Hash:44419000
Assign Vorher d: 444 Constant Hash:41149443
Assign sId Hash: -842352708 Constant Hash:41149443
Assign Nachher d: 444 Constant Hash:41149443
Calculate Nachher d: 00 Constant Hash:44419000
Calculate Hash in Calculate d: -842352708 Constant Hash:44419000
Calculate Vorher e: 00 Constant Hash:44419000
Assign Vorher e: 555 Constant Hash:41149443
Assign sId Hash: -842352709 Constant Hash:41149443
Assign Nachher e: 555 Constant Hash:41149443
Calculate Nachher e: 00 Constant Hash:44419000
Calculate Hash in Calculate e: -842352709 Constant Hash:44419000
Calculate Vorher f: 00 Constant Hash:44419000
Assign Vorher f: 666 Constant Hash:41149443
Assign sId Hash: -842352710 Constant Hash:41149443
Assign Nachher f: 666 Constant Hash:41149443
Calculate Nachher f: 00 Constant Hash:44419000
Calculate Hash in Calculate f: -842352710 Constant Hash:44419000
Calculate Vorher g: 00 Constant Hash:44419000
Assign Vorher g: 777 Constant Hash:41149443
Assign sId Hash: -842352711 Constant Hash:41149443
Assign Nachher g: 777 Constant Hash:41149443
Calculate Nachher g: 00 Constant Hash:44419000
Calculate Hash in Calculate g: -842352711 Constant Hash:44419000
Calculate Vorher h: 00 Constant Hash:44419000
Assign Vorher h: 888 Constant Hash:41149443
Assign sId Hash: -842352696 Constant Hash:41149443
Assign Nachher h: 888 Constant Hash:41149443
Calculate Nachher h: 00 Constant Hash:44419000
Calculate Hash in Calculate h: -842352696 Constant Hash:44419000
Calculate Vorher i: 00 Constant Hash:44419000
Assign Vorher i: 999 Constant Hash:41149443
Assign sId Hash: -842352697 Constant Hash:41149443
Assign Nachher i: 999 Constant Hash:41149443
Calculate Nachher i: 00 Constant Hash:44419000
Calculate Hash in Calculate i: -842352697 Constant Hash:44419000
T: 1
F: 0
true: 1
false: 0
a: 0
b: 0
c: 0
d: 0
e: 0
f: 0
g: 0
h: 0
i: 0
Der Thread 0x16cc hat mit Code 0 (0x0) geendet.
Der Thread 0xd0 hat mit Code 0 (0x0) geendet.
Das Programm "[5004] MathParserTest.vshost.exe: Verwaltet" wurde mit Code 0 (0x0) beendet. |
Ich habe zwei equivalente Ausgaben rot Markiert, wie man beim zweiten im Assign Teil sieht, sind die Werte aus dem ersten Teil schon vorbelegt, haben aber außerhalb der Methode andere Werte. Im Debugger kann ich dies jedoch nicht nachvollziehen. Auch der HashValue von Constants ist in diesem Bereich der des Wertes vom ersten durchlauf.
Leert man das Constants Dictionary im Destruktor, erhält man beim Assign immer KeyNotFound.
|
|
12.02.2010 14:48
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hallo diamond,
ja, das liegt an der Erkennung von Operatoren mit mehr als einem Zeichen (GetOperator). Du könntest, wenn du nur Operatoren mit genau einem Zeichen verwendest (so wie der Standard-FormulaParser), die Methode so abändern:
C#-Code: |
Operator GetOperator(ref char c, OpType op_type, OpPriority op_pri)
{
if(!IsOpChar(c))
return null;
c = GetNextChar();
return IsOperator(c.ToString(), op_type, op_pri);
}
|
Dies war (in etwa) die Originalversion.
Die meisten Formelparser in den Compilern verwenden die sog. "Longest Prefix"-Methode - dies ist mir jedoch hierfür zu aufwendig.
Alternativ könnte man noch ein Flag einbauen, so daß je nach konkreten Parser entschieden wird, welche Methode genommen wird.
Oder aber, die obige Methode wird in die bisherige integriert (quasi als Fallback):
C#-Code: |
Operator GetOperator(ref char c, OpType op_type, OpPriority op_pri)
{
if(!IsOpChar(c))
return null;
IEnumerator<char> functionator = (IEnumerator<char>)((CharEnumerator)Functionator).Clone();
char cOp = c;
string sOp = GetOperator(ref c);
Operator op = IsOperator(sOp, op_type, op_pri);
if(op == null)
{
Functionator = functionator;
c = cOp;
op = IsOperator(c.ToString(), op_type, op_pri);
if(op != null)
c = GetNextChar();
}
return op;
}
|
Ausprobiert habe ich es jetzt noch nicht, aber du kannst es ja mal testen und mir Bescheid geben.
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Th69 am 11.05.2010 16:40.
|
|
11.05.2010 11:09
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
diamond
myCSharp.de-Mitglied
Dabei seit: 29.06.2007
Beiträge: 38
Entwicklungsumgebung: C#, VB6, Perl, PHP Herkunft: Deutschland
|
|
nein leider bringt die fallback zeile noch nichts.
Meine erste Idee war das Operatoren Array zu erweitern, mit
C#-Code: |
static new Operator[] Operators =
{
...
new Operator("*-", MulNegativ, OpType.Binary, 2),
new Operator("/-", DivNegativ, OpType.Binary, 2),
new Operator("*+", Mul, OpType.Binary, 2),
new Operator("/+", Div, OpType.Binary, 2)
};
|
MulNegativ und DivNegativ haben es dann errechnet, in dem Moment hatte ich nur noch nicht gemerkt, dass es noch andere Konstellationen geben könnte :-(
Wenn die Operatoren mit Whitespaces gekapselt sind, klappt es ja, daher meine nächste Idee, den Formula String vor dem Parsen Char für Char durchzugehen und bei jedem operator ein Whitespace vor zu setzen.
Wodurch aus 3*-3 ein 3 * -3 entstehen würde.
Was meinst du dazu?
C#-Code: |
public T Parse(Str function, T x)
{
function = NormalizeFormula(function);
sFunction = function;
xValue = x;
Functionator = function.GetEnumerator();
char c = '\0';
T y = Expression(ref c);
if(c != '\0')
throw new FctException("Unexpected character at end of expression", c);
return y;
}
|
C#-Code: |
public Str NormalizeFormula(Str function)
{
Str newFunction = String.Empty;
IEnumerator<char> e = function.GetEnumerator();
while (e.MoveNext())
{
char chr = e.Current;
bool isOp = false;
IEnumerator<Operator> opEnum = Operators.GetEnumerator();
while (opEnum.MoveNext())
{
Operator op = opEnum.Current;
if (op.sOp == chr.ToString())
{
isOp = true;
break;
}
}
if (isOp)
newFunction += " " + chr;
else
newFunction += chr.ToString();
}
function = newFunction;
return function;
}
|
|
|
11.05.2010 15:38
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hallo diamond,
leider halte ich davon garnichts, da ich die Eingangsformel unangetastet lassen möchte.
Ich habe aber jetzt mal meine Fallback-Variante selber ausprobiert. Es fehlten noch 2 Zeilen:
C#-Code: |
if(op != null)
c = GetNextChar();
|
Ich habe es im obigen Beitrag auch editiert.
Nun funktionieren auch "3*-3", "3--3" etc.
(nur, wie schon geschrieben, bei längeren Operatoren wie "**" bzw. "<<" und ">>" (beim IntParser) würden Ausdrücke wie "3**-3" eine Exception verursachen!)
|
|
11.05.2010 16:51
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 5 Monate. |
Aratar
myCSharp.de-Mitglied
Dabei seit: 28.10.2009
Beiträge: 118
|
|
Hallo,
Echt gute Arbeit. Was ich noch nützlich finden würde, wäre, wenn es auch komplexe Zahlen oder Gleichungen mit komplexen Zahlen lösen könnte. Aber ja, wird wahrscheinlich nicht gerade jeder brauchen.^^
Gruss Aratar
|
|
28.10.2010 21:12
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 2 Monate. |
|
Hallo alle zusammen,
habe gerade durch Zufall entdeckt, daß dieser Thread nun zu den "oberen Zehntausend" gehört (hat wirklich lange gedauert sooft zu klicken... - kleiner Scherz ;-) und angesichts des heutigen Datums wünsche ich allen Forumsmitgliedern auch gleich ein Frohes Neues Jahr.
Ich bedanke mich auch recht herzlich, daß an dieser Komponente so ein reges Interesse besteht und schon mehr als 500 mal heruntergeladen wurde.
P.S. Falls Leser sich wundern, warum ich nicht direkt auf den Beitrag von Aratar eingegangen bin - wir haben dies per PM erörtert, aber ich habe noch keine Rückmeldung erhalten, ob die Einbindung (Überschreiben von ParseNumbers) wirklich geklappt hat.
|
|
01.01.2011 18:36
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 8 Monate. |
JCH2k
myCSharp.de-Mitglied
Dabei seit: 13.09.2011
Beiträge: 1
|
|
Sehr cooles Teil!
Funktioniert richtig gut, auch mit komplizierteren Formeln!
So habe ich die flexibilität, die ich brauche (Formel in XML Konfiguration statt fest im Programmcode)...
Eine Frage hätte ich aber noch: Wie steht es mit der Lizenz? Darf ich den Code auch
in mein (kommerzielles) Programm einbauen?
Viele Grüße
Claudius
|
|
19.09.2011 17:12
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegt mehr als ein Jahr. |
|
Zuersteinmal wünsche ich allen ein Frohes Neues Jahr.
Gerade einmal 2 Jahre sind vergangen und weitere 10.000 Hits und insgesamt mehr als 1.100 Downloads - danke (leider habe ich keine weitere Rückmeldung erhalten, ob und inwiefern dieser Parser eingesetzt wird bzw. ob noch Verbesserungspotential besteht).
|
|
02.01.2013 12:13
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 6 Monate. |
Andreas Adler
myCSharp.de-Mitglied
Dabei seit: 18.12.2012
Beiträge: 29
|
|
Hallo Th69,
vielen Dank für diesen super Formel-Parser. Vor allem die Performance ist genial!
Ich verwende den Parser in einer Reporting-Lösung, in der bisher nur die Grundrechenarten verwendet werden. Allerdings haben wir jetzt die Anforderung, auch komplexere Formeln auszuwerten, die auch Funktionen unterstützen sollen.
Also zum Beispiel: "If(Bruttoumsatz = 0, 0, Nettoumsatz / Bruttoumsatz)", wobei die beiden Variablen Bruttoumsatz und Nettoumsatz vor der Formelauswertung durch den konkreten Wert ersetzt werden, tatsächlich wäre die auszuwertende Formel also z.B. "If(100 = 0, 0, 75 / 100)".
Meine Frage wäre jetzt also, ob sich dein Formel-Parser mit relativ wenig Aufwand so anpassen ließe, dass er eben auch Funktionen und Vergleichsoperation (größer, gleich, ungleich usw.) unterstützt oder würdest du sagen, dass das dein Formel-Parser einen ganz anderen Ansatz verfolgt und sich das nicht so leicht als Add-On realisieren lässt?
Wäre schön, wenn du dazu kurz deine Einschätzung schreiben könntest.
Falls es gehen würde, könntest du mir einen Anhaltspunkt geben, wo ich eingreifen müsste?
Vielen Dank schon mal vorab! :)
|
|
19.07.2013 16:49
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Wenn du die gewünsche Syntax ein wenig änderst ist es ziemlich einfach. Wenn du auch mit If(Bruttoumsatz, 0, 0, Nettoumsatz / Bruttoumsatz) zufrieden bist kannst du das ziemlich einfach ergenzen.
Einfach zu den Funktionen eine neue "If" Funktion hinzufügen
C#-Code: |
new Function("if", If)
|
Und diese dann so definieren:
C#-Code: |
static double If(double[] arguments)
{
if(arguments == null || arguments.Length != 4) throw new ArgumentException("arguments");
return Math.Abs(arguments[0] - arguments[1]) < arguments[0]*1e-15? arguments[2] : arguments[3];
}
|
Und schon funktioniert es.
Edit: Da muss ich gleich noch etwas kritik loswerden. Der ConditionalParser ist für Fließkommazahlen so wie er jetzt ist nicht geeignet. Zahlen mit == zu vergleichen ist keine gute idee, aufgrund der Ungenauigkeit die dadurch auftritt, wie der Computer diese Zahlen im Speicher hällt. Wenn man versucht im Beispielprogramm "1.9 - 0.9 == 1" zu prüfen, so ist das Ergebnis false.
Zahlen vergleicht man am besten so wie ich es im oberen Code getan habe. Differenz bilden, den Absolutwert nehmen, und dann prüfen ob der Unterschied der Zahlen so klein ist,dass man sie als "gleich" ansehen kann
Lieben Gruß
pdelvo
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von pdelvo am 19.07.2013 19:01.
|
|
19.07.2013 18:50
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hallo Andreas,
vielen Dank für das Lob.
Wie pdelvo geschrieben hat, kannst du einfach neue Funktionen definieren. Und die Variablen, z.B. Nettoumsatz und Bruttoumsatz, kannst du einfach der Konstantenliste hinzufügen und vor jedem Parse-Aufruf entsprechend die Werte anpassen.
Und pdelvo: der ConditionalParser war auch eher für Ganzzahlen gedacht. Wegen dem Typparameter T habe ich eben nur die allgemeingültigen Methoden Equals und CompareTo benutzt. Aber ein auf Fließkommazahlen angepasster Parser läßt sich ja leicht erstellen ;-)
|
|
19.07.2013 19:53
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Andreas Adler
myCSharp.de-Mitglied
Dabei seit: 18.12.2012
Beiträge: 29
|
|
Hallo pdelvo, hallo Th69,
danke für die Hinweisse. Gut zu wissen, dass man eigene Funktionen so leicht hinzufügen kann.
Leider klappt das im Falle der gewünschten If-Funktion nicht. Diese soll nämlich beliebige logische Ausdrücke auswerten können, nicht nur Gleichheit. Und auch die Syntax muss so bleiben wie im Beispiel.
Grund: die erwähnte Reporting-Lösung hat mittlerweile eine Excel-Ausgabe, in der die Werte allerdings nicht statisch drinstehen, sondern aus einer Tabelle gelesen werden, die wiederrum mit der Datenbank verknüpft ist. In einigen Zellen wird der Wert aus der Tabelle ermittelt (Kontenwert), in anderen Zellen stehen Formeln, die auf Basis der Kontenwerte Summen bilden und Prozentberechnungen durchführen.
Neben der Excel-Ausgabe gibt es aber auch statische Ausgaben (direkt im Programm oder z.B. als HTML-Bericht). In diesen Berichten werden die enthaltenen Formeln entsprechend berechnet. Beide Ausgabemodule haben aber eine gemeinsame Basis, daher müssen die Formeln mit Excel kompatibel sein (wir wollen einen eingeschränkten Umfang der Excel-Funktionen zur Verfügung stellen).
Jetzt kann der Anwender in manchen Bereichen eigene Formeln definieren, und da sollte er eben so ähnlich arbeiten können wie mit Excel (bis auf die Funktionsnamen, die in Excel lokalisiert sind, sollte es gleich sein).
Was ich also bräuchte wäre eine Mischung aus dem ConditionalParser und dem FunctionParser. Liegt das im Bereich des Möglichen mit dem MathParser?
Ich hatte auch eine Bibliothek gefunden, die mir Excel-Ausdrücke auswerten kann, aber bei einem Performance-Vergleich mit dem Mathparser (bei einfachsten Funktionen: Addition, Substraktion, Multiplikation und Division von zwei Zahlen) lagen Welten zwischen beiden Komponenten, deswegen ist letztere keine wirkliche Option für uns.
Gruß,
Andreas
|
|
20.07.2013 17:33
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hallo Andreas,
möglich ist alles, jedoch kann ich den Aufwand nicht vorhersagen.
Die Schwierigkeit besteht darin, daß bisher mein Parser nur auf genau einem Datentypen agiert, d.h. auch der ConditionalParser arbeitet nur auf dem zugrundeliegenden Datentypen T und nicht auf 'bool'.
Probiere aber einfach mal den ConditionalParser in Verbindung mit dem FormulaParser aus. Am besten du erzeugst dir aus dem Code vom ConditionalParser einen neuen Parser 'ExcelParser' o.ä. und schmeißt die nicht benötigten Funktionen / Operatoren raus:
C#-Code: |
public class ExcelParser : FctParser<double>
{
FormulaParser parser = new FormulaParser();
}
|
Und bei den benötigten Methoden Less, Equal, Greater, ... änderst du den Datentypen fix auf double und implementierst sie (ähnlich wie pdelvo vorgeschlagen hat), z.B.
C#-Code: |
const double Epsilon = 1e-15;
static double Equal(double x, double y)
{
return (double)(Math.Abs(x - y) < x * Epsilon);
}
|
Und dort fügst du dann noch deine if-Funktion hinzu (auf Grundlage der von pdelvo geschriebenen Methode, aber jetzt nur noch mit 3 Argumenten):
C#-Code: |
static double If(double[] arguments)
{
if(arguments == null || arguments.Length != 3) throw new ArgumentException("arguments");
return !Equal(arguments[0], 0)? arguments[1] : arguments[2];
}
static new Function[] Functions =
{
new Function("if", If)
}
|
Viel Erfolg dabei...
|
|
20.07.2013 18:39
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Zwischen diesen beiden Beiträgen liegen mehr als 7 Monate. |
Andreas Adler
myCSharp.de-Mitglied
Dabei seit: 18.12.2012
Beiträge: 29
|
|
Hallo Th69,
das Projekt Excel-Parser habe ich erstmal auf Eis gelegt, da es letztlich nur "nice to have" und kein "must have" war.
Dafür benutze ich deinen FormulaParser sowie den ConditionalParser und alles in allem leistet dein Parser super Arbeit.
Mir sind jetzt aber zwei Probleme aufgefallen, vielleicht könntest du (oder gerne auch jemand anderes) ein wenig Licht auf die Sache werfen.
Ich verwende den ConditionalParser<double, FormulaParser>.
Nehmen wir einmal folgende Formel an: ((1) + (-1)) == 0; hier erhalte ich die Fehlermeldung "Missing bracket: )". Aber eigentlich fehlt ja keine Klammer. Auf die Klammern um die Zahlen herum könnte man zwar verzichten (dann funktioniert die Auswertung der Formel auch), aber dennoch sollte das ja kein Problem darstellen, oder?
Das ist jetzt kein dramatisches Problem, man kann es ja leicht umgehen. Ich wollte es trotzdem erwähnen, weil es sich für mich wie ein Bug darstellt.
Das eigentlich gewichtigere Problem das ich habe ist der Vergleich von double-Werten, wobei die double-Ungenauigkeit ein falsches Ergebnis liefert.
Z.B. habe ich folgende Formel: (5819.03 - 2666 - 3153.03) == 0; der linke Vergleichswert ergibt eigentlich den Wert 0; dadurch müsste die Prüfung auf Gleichheit mit dem rechten Wert true zurückgeben. Tatsächlich liefert in diesem Fall dein Parser das Ergebnis false; was sehr ärgerlich ist, weil das Ergebnis nicht richtig ist (und somit meine Anwendung "falsch" funktioniert).
Ich hab mir die Equal-Methode im Conditional-Parser angeschaut; bei der o.a. Formel kommt für x der Wert -0.00000000000045474735088646412 und für y der Wert 0.0. Die Überprüfung x.Equals(y) ist dementsprechend aus Sicht des Computers richtig.
Ich hab hier auch testweise (im Debugger) die Überprüfung unter Berücksichtigung der Ungenauigkeit durchgeführt, die du und pdelvo in den vorherigen Beiträgen erwähnt habt, mit Epsilon = 1e-15; aber auch hier ist das Ergebnis noch false.
Gibt es für dieses Problem eine einfache und korrekte Lösung?
Danke und Gruß,
Andreas
|
|
14.03.2014 16:54
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hi Andreas Adler,
ich schätze mal, für das Problem gibt es keine einfache und zuverlässige Lösung. Der Wert des Epsilons muß so gewählt sein, daß es größer ist, als die bei der Berechnung enstehenden Ungenauigkeiten. Und die hängen wiederum von den Werten ab, mit denen du rechnest. Denn höhere Werte führen zu höheren Ungenauigkeiten. Also müßtest du dir ein Epsilon festlegen, welches deinen Erfordernissen entspricht. Dann könntest du diese Gleichung:
Code: |
1:
|
x - y == 0 |
|
so umschreiben:
Code: |
1:
|
Abs(x - y) < Epsilon |
|
Christian
|
|
14.03.2014 18:24
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
Hallo Andreas,
bzgl. des ConditionalParsers kommt der Parser mit den zwei Hierarchie-Ebenen (Vergleiche <-> Formeln) bei den Klammern durcheinander. Dazu müßte man stattdessen wohl einen Parser schreiben (der dann aber auch wieder die Unterscheidung zwischen bool und T kennen müßte).
Und bzgl. des double-Vergleichs hätte ich zwei Vorschläge:
- Umstellung auf decimal (anstatt double), da du ja anscheinend mit Geldwerten (Konten) agierst
- In Anlehnung an The Floating-Point Guide - Comparison die Vergleichsfunktion so schreiben:
C#-Code: |
public static bool NearlyEqual(double a, double b, double epsilon)
{
if (a == b)
return true;
double diff = Math.Abs(a - b);
if (a == 0 || b == 0)
{
return diff < epsilon;
}
else
{
double absA = Math.Abs(a);
double absB = Math.Abs(b);
return diff / (absA + absB) < epsilon;
}
}
|
Es wird also der relative Fehler verglichen, außer einer der beiden Werte ist 0!
Evtl. mußt du dann doch einen anderen (mächtigeren) Parser, wie muparser - fast math parser library, NCalc - Mathematical Expressions Evaluator for .NET oder Mathos Parser benutzen!?
PS: Ich habe gerade gesehen, daß ich genau 2000 Beiträge hier im Forum verfasst habe, dies dann also mein Beitrag #2001 ist, und das nach knapp 6 Jahren (d.h. fast jeden Tag ein Beitrag - ich bin wohl forumssüchtig?).
|
|
16.03.2014 11:02
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Andreas Adler
myCSharp.de-Mitglied
Dabei seit: 18.12.2012
Beiträge: 29
|
|
Hallo,
danke für die Antworten und den Hinweis auf die anderen Parser.
Ja du hast Recht, ich arbeite mit Geldwerten; über eine Umstellung von double auf decimal hatte ich auch schon nachgedacht; ich glaub ich hatte es sogar schon einmal versucht, bin aber an irgendeinem Punkt gescheitert... Aber ich kann das ja nochmal versuchen.
Eine andere Idee von mir war, die Beträge vor dem Vergleich auf die nötige Genauigkeit zu runden, also zwei Stellen. Dazu müsste man die round-Methode noch leicht anpassen (was kein Problem ist), sodass sie zwei Parameter entgegennimmt, einmal den Betrag und einmal die Anzahl der Dezimalstellen.
Ich werde mal schauen, ich denke die Umstellung auf decimal wäre generell in meinem Anwendungsfall von Vorteil. Das werde ich als nächstes versuchen.
Danke!
[offtopic]P.S.: Glückwunsch zu 2000 Beiträgen! Aber ich glaube von einer Forumssucht kann man bei einem Beitrag pro Tag noch nicht sprechen. ;)[/offtopic]
|
|
16.03.2014 13:36
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
|