Einen besseren Taschenrechner programmieren

Im ersten Beitrag hatte ich gezeigt, wie man mit Hilfe der Postfix, bzw. Polnischen Notation einen einfachen Taschenrechner programmieren kann. Dieser hatte allerdings einige Schwächen, da er einige allgemein anerkannte Rechenregeln wie Punkt-vor-Strich nicht respektiert, mit negativen Zahlen nicht sauber zurecht kommt und auch noch andere Schwächen besitzt. In diesem zweiten Teil sollen diese Mängel ausgebügelt werden und der Taschenrechner deutlich ausgebaut werden.

Die Beispiele zur Programmierung eines Taschenrechners auf dieser Seite sind in JavaScript programmiert. Eine PHP Version folgt.

Zunächst einmal wird das Ranking der einzelnen Operatoren festgelegt. Punktrechnung geht vor Strichrechnung. Ein Exponent geht sogar noch vor der Punktrechnung:

// Wertigkeit der Operatoren
var ranking = {'+': 4, '-': 4, '*': 3, '/': 3, '^': 2, '(': 0};


In meiner Notation ist ein niedriger Zahlenwert höherrangig. Den höchsten Rang belegt die schließende Klammer, gefolgt vom Zeichen für einen Exponenten “^”, anschließend gleichrangig die Punktrechenarten und zuletzt die Strichrechnungen. [Wer nun hier den Wert 1 vermisst, der sei für den Moment noch vertröstet, da es noch einen Operator gibt, der höherwertiger ist, als der Exponent.]

Der Code zur Umwandlung eines Terms in die Postfix Notation muss nun an zwei Stellen angepasst werden.

// ansonsten: 3. Prüfung: Handelt es sich zu Beginn des Eingabestrings um einen Operator
} else if (operator = eingabe.match(/^[\+\-\*\:\^]/)) {
        // Falls ja, aus dem Eingabestring entfernen
    eingabe = eingabe.substring(1);
            
    // Wenn frühere Operatoren vorliegen und der vorherige Operator einen gewichtigeren (kleineren) Rang hat
    if (operatorstack.length && ranking[operatorstack[operatorstack.length - 1]] < = ranking[operator[0]]) {
        // vorherigen Operator vom Stack holen
        lastoperator = operatorstack.pop();
        // und falls es sich dabei nicht um eine Klammer handelt, in den Ausgabespeicher schreiben
        if (lastoperator != '(') postfix.push(lastoperator);
    }
    // Nun den aktuellen Operator oben auf den Operatorstapel legen
    operatorstack.push(operator[0]);

und

// ansonsten: 4. Prüfung: Handelt es sich zu Beginn des Eingabestrings um eine Klammer ')'
} else if (eingabe.match(/^\)/)) {

    // Falls ja, dann den obersten Operator aus dem Operatorstapel holen
    lastoperator = operatorstack.pop();
    // und falls es sich dabei nicht um eine Klammer handelt, in den Ausgabespeicher schreiben
    if (lastoperator != '(') postfix.push(lastoperator);
    // und die Klammer aus dem Eingabestring entfernen
    eingabe = eingabe.substring(1);


Im ersten Codeblock wurde zunächst der Operator “^” für Potenzrechnungen mit aufgenommen. Dies ist dient der Funktionserweiterung des Taschenrechners. Anschließend findet ein Vergleich der Wertigkeit des aktuellen mit dem vorhergehenden Operators statt. Hatte der vorhergehende Operator eine höhere oder gleiche Wertigkeit (niedrigeres Ranking), wird dieser Operator in der Postfixfolge nun ausgegeben, bevor der neue Operator abgelegt wird. Eine öffnende Klammer wird nun auch als Operator betrachtet, jedoch nicht in die Postfix Ausgabe geschrieben. Dies wird erforderlich, da die Klammerung das höchste Ranking hat und auf diese Weise elegant mit berücksichtigt werden kann.

Als Beispiel kann der Term “1^2*3+4” dienen, der gleichbedeutend ist mit 1^{2}cdot3+4. In diesem Fall nimmt die Wertigkeit der Operatoren von links nach rechts ab, was zu keiner besonderen Herausforderung bei der Berechnung darstellt. Dreht man dieses jedoch um (4+3*1^2) wird die Berechnung spannender. In Polnischer Notation sieht dies dann wie folgt aus: 4; 3; 1; 2; ^; *; +.

Der zweite Codeblock wurde in soweit angepasst, dass auch hier vermieden wird, dass öffnende Klammern in die Postfix Ausgabe gelangen, da die Klammer auch auf den Operatorstapel landet.

Bleibt für den Taschenrechner also nur noch das Problem mit den negativen Zahlen. Hier bietet sich ein einfacher Trick an, indem eine Subtraktion in eine Addition einer negativen Zahl umgewandelt wird, also 1-2=1+(-2).

// ansonsten: 2. Prüfung: Handelt es sich zu Beginn des Eingabestrings um eine (ganze) Zahl
} else if (zahl = eingabe.match(/^\-?([0-9]+\.)?[0-9]+/)) {
    // Falls ja aus dem Eingabestring entfernen
    eingabe = eingabe.substring(zahl[0].length);
            
    // Falls es sich um eine negative Zahl handelt, dann wird hieraus eine Addition erzeugt
    if (zahl[0].charAt(0) == '-') {
        // Wenn frühere Operatoren vorliegen und der vorherige Operator einen gewichtigeren (kleineren) Rang hat
        if (operatorstack.length && ranking[operatorstack[operatorstack.length - 1]] < = ranking['+']) {
            // vorherigen Operator vom Stack holen
            lastoperator = operatorstack.pop();
            // und falls es sich dabei nicht um eine Klammer handelt, in den Ausgabespeicher schreiben
            if (lastoperator != '(') postfix.push(lastoperator);
        }
        operatorstack.push('+');
    }
            
    // Anschließend die Zahl in den Ausgabespeicher schreiben
    postfix.push(parseFloat(zahl[0]));

Geändert wurde wie folgt:

  • Zeile 32: Der reguläre Ausdruck wurde dahingehend angepasst, dass nun auch negative Zahlen und reelle Zahlen erkannt werden.
  • Zeile 37: Wenn es sich um eine negative Zahl handeln sollte, muss das Pluszeichen für die Addition eingearbeitet werden. Hierzu wird eine Prüfung des vorangegangenen Operators erforderlich, wie dies auch schon weiter oben erfolgt ist.
  • Zeile 49: wurden vorher nur integer (ganzzahlige) Werte genommen, müssen nun natürlich auch Fließkommazahlen (reelle Zahlen) genommen werden, daher parseFloat().

Eine weitere Schwachstelle ergibt sich am Anfang des Terms, wenn mit einer negativen Zahl begonnen wird. Um dies abzufangen wird vor dem Term ein “0+” gehangen, wenn das erste Zeichen ein Minus ist:

// Eingabe vorbelegen, wenn mit einer negativen Zahl begonnen wird
if (eingabe.charAt(0) == '-') eingabe = '0+' + eingabe;

Zur besseren Übersicht wurde der Quellcode nun in eine eigene Funktion gegossen und sieht damit wie folgt aus:

/**
* wandelt einen Term ín die Postfix Notation um 
*
* @param String eingabe der zu berechnende Term, Klammerung der Punktrechnungen erforderlich
* @return Array
*/
function strtopostfix(eingabe) {
    // Wertigkeit der Operatoren
    var ranking = {'+': 4, '-': 4, '*': 3, '/': 3, '^': 2, '(': 0};
    
    // Ergebnisspeicher für die Postfix Notation der Eingabe
    var postfix = new Array();

    // Pufferspeicher für die Operatoren während der Umwandlung
    var operatorstack = new Array();

    // Fehler im Eingabestring
    var fehlerfrei = true;

    // Eingabe vorbelegen, wenn mit einer negativen Zahl begonnen wird
    if (eingabe.charAt(0) == '-') eingabe = '0+' + eingabe;
    
    // Hauptschleife, die so lange ausgeführt wird, bis der Eingabestring leer ist
    while (fehlerfrei && eingabe.length) {

        // 1. Prüfung: Ist das erste Zeichen eine Klammer '('
        if (eingabe.match(/^
*** QuickLaTeX cannot compile formula:
/)) {
            // Falls ja, dann ignorieren, bzw. aus dem Eingabestring entfernen
            eingabe = eingabe.substring(1);

        // ansonsten: 2. Prüfung: Handelt es sich zu Beginn des Eingabestrings um eine (ganze) Zahl
        } else if (zahl = eingabe.match(/^\-?([0-9]+\.)?[0-9]+/)) {
            // Falls ja aus dem Eingabestring entfernen
            eingabe = eingabe.substring(zahl[0].length);
            
            // Falls es sich um eine negative Zahl handelt, dann wird hieraus eine Addition erzeugt
            if (zahl[0].charAt(0) == '-') {
                // Wenn frühere Operatoren vorliegen und der vorherige Operator einen gewichtigeren (kleineren) Rang hat
                if (operatorstack.length && ranking[operatorstack[operatorstack.length - 1]] < = ranking['+']) {
                    // vorherigen Operator vom Stack holen
                    lastoperator = operatorstack.pop();
                    // und falls es sich dabei nicht um eine Klammer handelt, in den Ausgabespeicher schreiben
                    if (lastoperator != '(') postfix.push(lastoperator);
                }
                operatorstack.push('+');
            }
            
            // Anschließend die Zahl in den Ausgabespeicher schreiben
            postfix.push(parseFloat(zahl[0]));

        // ansonsten: 3. Prüfung: Handelt es sich zu Beginn des Eingabestrings um einen Operator
        } else if (operator = eingabe.match(/^[\+\-\*\:\^]/)) {
            // Falls ja, aus dem Eingabestring entfernen
            eingabe = eingabe.substring(1);
            
            // Wenn frühere Operatoren vorliegen und der vorherige Operator einen gewichtigeren (kleineren) Rang hat
            if (operatorstack.length && ranking[operatorstack[operatorstack.length - 1]] <= ranking[operator[0]]) {
                // vorherigen Operator vom Stack holen
                lastoperator = operatorstack.pop();
                // und falls es sich dabei nicht um eine Klammer handelt, in den Ausgabespeicher schreiben
                if (lastoperator != '(') postfix.push(lastoperator);
            }
            // Nun den aktuellen Operator oben auf den Operatorstapel legen
            operatorstack.push(operator[0]);

        // ansonsten: 4. Prüfung: Handelt es sich zu Beginn des Eingabestrings um eine Klammer ')'
        } else if (eingabe.match(/^

*** Error message:
Missing $ inserted.

/)) { // Falls ja, dann den obersten Operator aus dem Operatorstapel holen lastoperator = operatorstack.pop(); // und falls es sich dabei nicht um eine Klammer handelt, in den Ausgabespeicher schreiben if (lastoperator != '(') postfix.push(lastoperator); // und die Klammer aus dem Eingabestring entfernen eingabe = eingabe.substring(1); // ansonsten wurde ein Zeichen gefunden, mit dem nichts angefangen werden kann: Fehler! } else { // gesonderte Abbruchbedingung der Hauptschleife setzen fehlerfrei = false; } } // Nach Abarbeitung des Eingabestrings ggf. noch verbliebene Operatoren in den Ausgabespeicher verschieben. while (operatorstack.length) { postfix.push(operatorstack.pop()); } // Wenn kein Fehler vorliegt wird das Array mit der Postfix Notation zurückgeliefert, ansonsten null return (fehlerfrei ? postfix : null); }


Am Code für die Auswertung, bzw. Berechnung des in Postfix Notation vorliegenden Terms für den Taschenrechner, hat sich nichts gewichtiges verändert. Lediglich die Potenzrechnung wurde exemplarisch hinzugefügt und insgesamt der Code ebenfalls in eine eigene Funktion gepackt, die als Wert ein Array in Postfix Notation erwartet.

/**
* berechnet aus einem Term in Postfix Notation das Ergebnis
*
* @param Array postfix Term in Postfix Notation
* @return Float
*/
function evalpostfix(postfix) {
    // Hilfs- und Ergebnisstapel
    var stack = new Array();

    // Hauptschleife, die so lange ausgeführt wird, wie Elemente im Postfix Array vorhanden sind
    while (postfix.length) {
        // erstes Element des Postfix Stapels ziehen und je nach Fall auswerten
        switch (element = postfix.shift()) {
            case ':':
                // Division
                b = stack.pop();
                stack.push(stack.pop() / b);
                break;
            case '*':
                // Multiplikation
                stack.push(stack.pop() * stack.pop());
                break;
            case '+':
                // Addition
                stack.push(stack.pop() + stack.pop());
                break;
            case '-':
                // Subtraktion
                b = stack.pop();
                stack.push(stack.pop() - b);
                break;
            case '^':
                // Potenzrechnung
                b = stack.pop();
                stack.push(Math.pow(stack.pop(), b));
                break;
            default:
                // Annahme, dass es sich nun nur noch um eine Zahl handeln kann
                stack.push(element);
        }
    }

    // Das Ergebnis der Berechnung liegt nun oben auf dem Hilfsstapel
    return stack.pop();
}

Aufgerufen wird der Taschenrechner in JavaScript nun wie folgt:

ergebnis = evalpostfix(strtopostfix(eingabe));

Der zu berechnende Term steckt in der Variablen “eingabe”, das Resultat wird dann in “ergebnis” gespeichert. Die Programmierung eines Taschenrechners, der mehr als die Grundrechenarten und auch korrekte Rechenregeln beherrscht, ist also nicht weiter schwierig.

Schreibe einen Kommentar

Nutze dieses Kommentarfeld um deine Meinung oder Ergänzung zu diesem Beitrag kundzutun. Verhalte dich bitte respektvoll und höflich! Kommentare werden vor der Veröffentlichung in der Regel moderiert und bei Verstößen gegen geltendes Recht, die guten Sitten, fehlendem Bezug oder missbräuchlicher Verwendung nicht freigegeben oder gelöscht.
Über die Angabe deines Namens, deiner E-Mail Adresse und deiner Webseite freuen wir uns, doch diese Felder sind optional. Deine E-Mail Adresse wird dabei zu keinem Zeitpunkt veröffentlicht.

Um mit dem Betreiber dieser Seite nicht-öffentlich in Kontakt zu treten, nutze die Möglichkeiten im Impressum.