Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Parser für mathematische Formeln
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

Parser für mathematische Formeln

beantworten | zitieren | melden

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

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:

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
Dieser Beitrag wurde 7 mal editiert, zum letzten Mal von Th69 am .
Attachments
private Nachricht | Beiträge des Benutzers
pdelvo
myCSharp.de - Member

Avatar #avatar-3354.png


Dabei seit:
Beiträge: 1407

beantworten | zitieren | melden

Super Snippet!



Gruß pdelvo
private Nachricht | Beiträge des Benutzers
TheBrainiac
myCSharp.de - Member

Avatar #avatar-3152.png


Dabei seit:
Beiträge: 832
Herkunft: /dev/null

beantworten | zitieren | melden

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.
Dieser Beitrag wurde 2 mal editiert, zum letzten Mal von TheBrainiac am .
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"
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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 .
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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).
Attachments
private Nachricht | Beiträge des Benutzers
ikaros
myCSharp.de - Member



Dabei seit:
Beiträge: 1787

beantworten | zitieren | melden

@Th69: Mir gefällt die Idee, es ist benutzbar. Weiter so.
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von ikaros am .
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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)")...
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Th69 am .
Attachments
private Nachricht | Beiträge des Benutzers
Zentauro
myCSharp.de - Member



Dabei seit:
Beiträge: 112
Herkunft: Linz/Oberösterreich

beantworten | zitieren | melden

echt geniales ding - DANKE für deine mühen!!!
private Nachricht | Beiträge des Benutzers
Cat
myCSharp.de - Member

Avatar #avatar-3070.jpg


Dabei seit:
Beiträge: 790

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Torgrimm
myCSharp.de - Member



Dabei seit:
Beiträge: 24
Herkunft: Aschaffenburg

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Torgrimm
myCSharp.de - Member



Dabei seit:
Beiträge: 24
Herkunft: Aschaffenburg

beantworten | zitieren | melden

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.
Attachments
private Nachricht | Beiträge des Benutzers
Torgrimm
myCSharp.de - Member



Dabei seit:
Beiträge: 24
Herkunft: Aschaffenburg

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
diamond
myCSharp.de - Member



Dabei seit:
Beiträge: 38
Herkunft: Deutschland

beantworten | zitieren | melden

@Th69

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

ReSpEkT!!
private Nachricht | Beiträge des Benutzers
diamond
myCSharp.de - Member



Dabei seit:
Beiträge: 38
Herkunft: Deutschland

beantworten | zitieren | melden

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?
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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.
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Th69 am .
private Nachricht | Beiträge des Benutzers
diamond
myCSharp.de - Member



Dabei seit:
Beiträge: 38
Herkunft: Deutschland

beantworten | zitieren | melden

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;
}
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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!)
private Nachricht | Beiträge des Benutzers
diamond
myCSharp.de - Member



Dabei seit:
Beiträge: 38
Herkunft: Deutschland

beantworten | zitieren | melden

Hallo Th69,

ok, so ist es natürlich viel besser. :-)


Thx
private Nachricht | Beiträge des Benutzers
Aratar
myCSharp.de - Member



Dabei seit:
Beiträge: 126

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
JCH2k
myCSharp.de - Member



Dabei seit:
Beiträge: 1

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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).
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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).
private Nachricht | Beiträge des Benutzers
Andreas Adler
myCSharp.de - Member



Dabei seit:
Beiträge: 30

beantworten | zitieren | melden

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! :)
private Nachricht | Beiträge des Benutzers
pdelvo
myCSharp.de - Member

Avatar #avatar-3354.png


Dabei seit:
Beiträge: 1407

beantworten | zitieren | melden

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
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von pdelvo am .
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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 ;-)
private Nachricht | Beiträge des Benutzers
Andreas Adler
myCSharp.de - Member



Dabei seit:
Beiträge: 30

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 3978

Themenstarter:

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
Andreas Adler
myCSharp.de - Member



Dabei seit:
Beiträge: 30

beantworten | zitieren | melden

Hallo Th69,

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

Gruß,
Andreas
private Nachricht | Beiträge des Benutzers