Laden...

Parser für mathematische Formeln

Letzter Beitrag vor 9 Monaten 40 Posts 80.718 Views
Parser für mathematische Formeln

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.


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.


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'):


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...

  1. 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:


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):


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);  // Binäre Operation ausführen
    }

    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:


using OpPriority = System.SByte;

// Function

protected delegate T Fct(T x);

protected class Function
{
     public string sFct;
     public Fct fct;
};

// Operator

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;
};

// Constant

protected class Constant
{
    public string sConst;
    public T value;
};

Zu jeder dieser Klassen gibt es eine Membervariable, welche eine Aufzählung beinhaltet:


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.


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;
    }
 
    // weitere Operatormethoden ...

    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:


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:


Eval<double> eval = new Eval<double>(); // bzw. (true) für interne Optimierungen
FormulaParser fp = new FormulaParser();

string sFct = "2*x+PI";
eval.Parse(fp, sFct);

string sOptFct = eval.ToString(); // Ausgabe der (optimierten) Formel

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

Super Snippet!

👍

Gruß pdelvo

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:

if (c == OperatorBracket.bra)
{ ... }
else if (IsDigit(c)) // Hier war vorher "Char.IsDigit(c) || c == Dot"
    ...
else if (Char.IsLetter(c))
{ ... }

Und die Methode bool IsDigit(char c) hinzufügen:

protected virtual bool IsDigit(char c)
{
    return Char.IsDigit(c) || c == Dot;
}

Damit sind die Grundlagen für folgenden Hex-Rechner gelegt:

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.

`There are 10 types of people in the world: Those, who think they understand the binary system Those who don't even have heard about it And those who understand "Every base is base 10"`

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

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).

@Th69: Mir gefällt die Idee, es ist benutzbar. Weiter so.

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...

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)")...

echt geniales ding - DANKE für deine mühen!!!

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.

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. 👍

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.


    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>();

            // Hinzufügen der benötigten der VariDataObjecte als Konstanten in den Parser - LeftFile
            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);
                }
            }

            // Die Geänderten Konstanten in die TransformFile übernehmen
            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
[color]
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
[/color]
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
[color]
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
[/color]
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.

Kleine Anmerkung noch:
Wenn man die Assign Methode und den '=' Operator vom ConditionalAssignmentParser in den FctParser verschiebt funktioniert das ganze so wie es soll.

Ich bin damit allerdings noch nicht so richtig zufrieden und werde das Thema wieder verfolgen, sobald ich etwas mehr Zeit habe.

@Th69

nun benutze ich deine Klasse auch! 😃
Ich muss schon sagen, einfach genial, vor allem die performance!

ReSpEkT!!

da scheint noch ein BUG zu sein.

nutze den FormulaParser und wenn ich versuche die folgenden Ausdrücke zu parsen, kommt er auf eine Exception

3*-3
3/-3

zwischen den beiden operatoren ist in meinem fall kein leerzeichen. sobald ein leerzeichen zwischen ist, funktioniert es wiederrum.

ich glaube zu dieser exception wird es immer kommen, wenn zwei operatoren hintereinander eingegeben werden, also auch beim ausdruck 3--3.

lässt sich das problem irgend wie lösen?

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:


// in FctParser.cs
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):


Operator GetOperator(ref char c, OpType op_type, OpPriority op_pri)
{
    if(!IsOpChar(c))
        return null;

    // save current Functionator and character
    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;

        // Fallback on single char
        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.

nein leider bringt die fallback zeile noch nichts.

Meine erste Idee war das Operatoren Array zu erweitern, mit

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?


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;
}


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;
}

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:


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!)

Hallo Th69,

ok, so ist es natürlich viel besser. 😃

Thx

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

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.

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

Hallo Claudius,

ich habe keine eigenen Lizenzbedingungen (außer der unter Lizenzbedingungen für .NET-Komponenten und C#-Snippets auf myCSharp.de ).
Und ich wünsche es mir sogar, daß mein Parser auch in kommerziellen Produkten eingesetzt wird (wobei es schön wäre, wenn ich - Th69 - in den Infos/Credits erwähnt werde bzw. ein Link auf diese Seite. Umgekehrt reicht es mir aber auch, wenn der Name des Produktes bzw. ein Link auf die offizielle WebSite hier oder per PN gepostet wird).

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).

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! 😃

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

new Function("if",   If)

Und diese dann so definieren:


        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

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 😉

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

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:


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.


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):


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...

Hallo Th69,

danke für die Hinweise. Ich werde mal schauen, ob ich damit einen geeigneten Excel-Parser implementieren kann.

Gruß,
Andreas

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

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:

x - y == 0

so umschreiben:

Abs(x - y) < Epsilon

Christian

Weeks of programming can save you hours of planning

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:

public static bool NearlyEqual(double a, double b, double epsilon)
{
	if (a == b) // shortcut, handles infinities
		return true;
	
	double diff = Math.Abs(a - b);

	if (a == 0 || b == 0)
	{
		// a or b is zero
		// relative error is less meaningful here
		return diff < epsilon;
	}
	else
	{
		double absA = Math.Abs(a);
		double absB = Math.Abs(b);

		// use relative error
		return diff / (absA + absB) < epsilon; // alternativ stattdessen Math.Max(absA, absB)
	}
}

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?).

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!

P.S.: Glückwunsch zu 2000 Beiträgen! Aber ich glaube von einer Forumssucht kann man bei einem Beitrag pro Tag noch nicht sprechen. 😉

Hallo Th69,

ich bin kein Programmierer, nur ein Großvater (82), der für seine Enkel einen einfachen Math Graph in C# geschrieben hat.

Läuft auch alles sehr gut.

Nur bei Eingabe der Funktion f(x) = x^(1/3) oder f(x) = x#3 berechnet er nur die positiven 
x-Werte und zeichnet auch nur die Kurve für die positiven x-Werte.

Kann an dem Math Parser noch etwas geändert werden oder lieg das Problem bei C# und dem 
Pow-Befehl?

MfG Volker

Das ist ja einfach mathematisch (für reelle Zahlen) unbestimmt, d.h. für negative x sind die beiden Funktionen Potenz und Wurzel nicht definiert, s. a. Potenzfunktion (auch dort sind im rechten Bild die Graphen bei diesen Funktionen nur für positive x dargestellt, z.B. bei der Wurzelfunktion x^(1/2) der schwarze Graph).

Die Wurzel aus negativen Zahlen ist ja nur bei Komplexen Zahlen definiert.

PS: Beruht dein C#-Programm auf meinem Graph-Programm (Graph.zip) aus dem Beitrag oder hast du ein komplett Neues erstellt?

Hallo Th69,

vielen Dank für die schnelle Antwort.

Alles richtig, was du schreibst.

Aber ich suche für mein Mathe-Graph eine Lösung für den Sonderfall „ungerade Wurzel aus negativer Zahl“

f(x) = x^(1/3) für X = -10 ... X = +10    Beispiel: i x = -8   f(x) = -8^(1/3) = -2

Da jeder Taschenrechner heute diese Aufgabe lösen kann,
hoffte ich, dass ein Math Parser solche Funktionen auch erkennt.
(siehe: GeoGebra Grafikrechner)

Die Idee für mein Mathe-Graph entspringt deinem Graph-Programm (Graph.zip).

Da ich keine Programmiererfahrung habe, ist mein C#-Programm eine Sammlung aus Fachbüchern, deinem Graph.zip-Programm und aus verschiedenen Internet-Beispielen.

Da dieser Code zum Teil im Netz veröffentlicht wurde, hoffe ich kein Copyright verletzt zu haben.

Code für Kurve berechnen und zeichnen:


        void CanvasView_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
 {
     SKSurface surface = e.Surface;
     SKCanvas canvas = surface.Canvas;
     if (z >= 1)
     {
         eingabe.Text = " ";
         canvas.Clear();
         canvas.DrawColor(SKColors.Transparent);
     }
     int width = e.Info.Width;
     int height = e.Info.Height;
     try
     {
         sFct = eingabe.Text;
         string sOptFct = eval.ToString();
         eval.Parse(fp, sFct);
     }
     catch (Exception)
     {
         return;
     }
     x0 = (int)(Xmin * width / (Xmin - Xmax));
     y0 = height - (int)(Ymin * height / (Ymin - Ymax));
     xx = (Xmax - Xmin) / width / tb;
     x1 = (float)x0;
     y1 = (float)y0;
     SKPoint p = new SKPoint(x1, y1);
     canvas.DrawPoint(p, FillPaint3);
     for (x = Xmax; x >= Xmin; x -= xx)
     {
         if (eingabe.Text == " ")
         { Loeschen_Clicked(sender, e); }
         try
         {
              y = eval.Evaluate(x);
              if (!Double.IsNaN(y)) { y = (float)y; }
              }
             catch (Exception)
             {
                 DisplayAlert("Achtung!", "Eingabefehler", "OK");
                 return;
             }   
         {
             if (y > 0.0 | y < 0.0)
             {
                 x1 = x0 + (int)(x * width / (Xmax - Xmin));
                 y1 = y0 - (int)(y * height / (Ymax - Ymin));
             }
             SKPoint p1 = new SKPoint(x1, y1);
             canvas.DrawPoint(p1, FillPaint3);
         }
         canvas.Save();
     }
 }

Zitat von VolkPT

Jetzt verstehe ich, du meinst diesen Spezialfall Potenzfunktion: Ungerade Wurzeln aus negativen Zahlen.

So funktioniert weder die Fließkommadarstellung (float, double) noch die Pow-Funktion:

  • bei 1/3wird direkt der ausgerechnete Wert 0.33333...übergeben (aus dem nicht direkt zurückgerechnet werden kann, um welche ganzzahligen Werte einer natürlichen Division es sich handelt)
  • die Pow(x, y)-Funktion berechnet intern den Wert aus e^(y * ln(x))und ln(x)ist für negative x nicht definiert

Mein Parser hat jedoch schon in FormulaParser die Methode Root(Zeile 88), die du dann anpassen kannst:

static double Root(double x, double y)
{
  if (x < 0)
     return -Math.Pow(-x, (double)1/y);
  else
     return Math.Pow(x, (double)1/y);
}

Nun sollte auch x#3für negative x-Werte funktionieren.

Falls du diese im Parser auch als Funktion aufrufen möchtest, anstatt nur über (den von mir selbst gewählten Operator #), so mußt du diese in FormulaParser dem statischen Functions-Array hinzufügen:

new Function("root", Root)

Aufruf dann z.B. mit root(x,3)bei der Parser-Eingabe.


PS: Dein Code erscheint mir noch verbesserungswürdig. Was soll z.B. Loeschen_Clicked(...) innerhalb der Schleife?

Hallo Th69,

vielen Dank für die Lösung.

Ich habe den Code Root-Methode (Zeile 88) noch ein wenig erweitert.

Die Abfrage y = gerade oder ungerade habe ich noch eingefügt, da sonst gerade Wurzeln aus negativen Zahlen berechnet wurden.

MfG VolkPt

 static double Root(double x, double y)
 {
     if ((x < 0) & (y % 2 != 0))

     return -Math.Pow(-x, (double) 1 / y);
     else
     return Math.Pow(x, (double) 1 / y);

 }