"Aus klein mach groß": Teil3 - Rechnen mit Zahlen

CDW

Moderator
Mitarbeiter
dies ist die Fortsetzung der Aufgabenreihe "Aus klein mach groß":
"Aus klein mach groß": Teil2 - Zahlen und Satzzeichen
"Aus klein mach groß": Teil1 - Stack
Und ist wie die ganze Reihe an Anfänger gerichtet.

Diesesmal nutzen wir die zwei vorherigen Aufgaben, um einen einfachen Rechner zu bauen, der die Eingaben im Postfix Format aktzeptiert.
Einige von uns kennen noch die alten Taschenrechner, die Eingaben in diesem Format erwartetet ;).
Dabei werden zuerst die Operanden und dann die Operatoren angegeben.
Bsp:
7 8 +
entspricht: 7+8=15
7 8 + 2 *
entspricht: (7+8 )*2
In dieser Notation braucht man also keine Klammern und auch keine "Gewichtung" der Operatoren.

Nutze also die Funktionen/Klassen aus der Teilaufgabe2 (ggf. anpassen/erweitern ), um ein UPN Rechner-Modul zu bauen.

Dabei bekommt das Rechner-Modul eine Eingabe als String
"1 2+" und gibt ein Ergebnis (oder eine Fehlermeldung) zurück.

Es sollten 4 Grundrechenarten akzeptiert werden: + - * /
Trennzeichen zwischen Operanden (also verschiedenen Zahlen): " " (Leerzeichen).
Trennzeichen zwischen Operanden und Operatoren/Operatoren: entweder Leerzeichen oder kein Trennzeichen.

Bsp: 1 2 + oder 1 2+

Implementiere zusätzlich zum Rechner-Modul ein Ausgabemodul, um
Zwischenergebnisse (optional: auch die durchgeführte Operation/Operatoren) anzuzeigen:
7 8+2 *
Ausgabe:
(7+8= )15 (in Klammern: optionale Ausgabe)
(15*2= )30
Ergebnis=30

Das Ausgabemodul sollte nur zur Fehlersuche/Überprüfung dienen und so integriert sein, dass es sich leicht (im Quellcode) "ausschalten" lässt (also die Anweisungen nicht "untrennbar" mit dem Rechner-Modul verbunden sind - somit bitte keine GUI/CUI anweisungen hineinquetschen, sondern einen Aufruf des Ausgabemoduls).

Gehe davon aus, dass die Eingabe "korrekt" ist, zumindest was das Zahlenformat angeht. Man muss also keine 123.123.123 Formen berücksitigen.

Allgemeine Tipps:
in der letzten Teilaufgabe haben wird aus einem Satz Zahlen und Satzzeichen ausgelesen. Diese Rotine lässt sich nun etwas anpassen (es waren nicht umsonst 4 Satzzeichen ;) ), so dass */+- erkannt werden. Zahlenerkennung kann so belassen werden (es sei denn, man hat RegExpressions verwendet *g*).

Man könnte nun die Eingabe durchgehen und Zahlen einlesen/im Stack ablegen. Kommt ein Operator(*/+-) so werden die letzen beiden Zahlen aus dem Stack geholt und miteinander verrechnet - das Ergebnis kommt wieder auf den Stack.
Wenn nun der String abgearbeitet wurde, liegt im Stack das Endergebnis.

Gibt es bei einer Operation nicht mehr genügend Operanden (z.B wenn noch + abgearbeitet werden muss, im Zahlenstack aber nur eine einzige Zahl steht) oder liegen auf dem Stack nach der Abarbeitung mehrere Zahlen , so war die Eingabe fehlerhaft ;)
 

Boar

New member
So, hier meine Lösung (Java).

Code:
public class UPNModul {

	// Der Zahlen-Stack
	private static Stack digits = new Stack();

	//Die Methode löst die eigebene Gleichung und gibt das Ergebnis als String zurück
	public static String solve(String line) throws RuntimeException {

		String number = ""; //Zwischenspeicher, um eine komplette Zahl zusammenzusetzn
		boolean floatFound = false; //Prüft, ob die Zahl schon einen Punkt hat
		while(!digits.isEmpty())
			digits.pop();
		
		for(int i=0; i<line.length(); i++) {
			
			char c = line.charAt(i); //Das aktuelle Zeichen
			
			//Ziffern oder der erste Punkt (Alle weiteren Punkte gehören nicht zur Zahl)
			if(Character.isDigit(c) || (c == '.' && !floatFound)) {
				number+=c; //Aktuelle Ziffer zum Zwischenspeicher hinzufügen
				if(c=='.') floatFound = true;
			}
			//Operatoren oder Leerzeichen
			else if(c == ' ' || c == '+' || c == '-' || c=='*' || c=='/') {
				//Die Zahl im Zwischenspeicher auf den Stack legen (wenn im Zwischenspeicher etwas steht)
				if(!number.equals("")) {
					numberInStack(number, floatFound);
					floatFound = false;
					number = "";
				}
				if(c == ' ') continue; //Wenn Leerzeichen: Prüfe das nächste Zeichen

				//Die 2 Zahlen aus dem Stack
				float digit1;
				float digit2;
				try {
					digit2 = (Float) digits.pop();
					digit1 = (Float) digits.pop();
				} catch (RuntimeException e) {
					//Wenn keine 2 Zahlen im Stack liegen (RuntimeException vom Stack) ist die Gleichung fehlerhaft
					throw new RuntimeException("Die Gleichung entspricht nicht der Postfixnotation");
				}
						
				//Je nach Operator wird das Ergebnis berechnet und wieder auf den Stack gelegt
				switch(c) {
				case '+': 
					digits.push(digit1 + digit2);
					break;
				case '-':
					digits.push(digit1 - digit2);
					break;
				case '*':
					digits.push(digit1 * digit2);
					break;
				case '/':
					digits.push(digit1 / digit2);
					break;
				}
			// Wenn es keine Ziffer, kein Leerzeichen und kein Operator war: Fehler
			} else {
				throw new RuntimeException("Falsche Eingabe: "+c);
			}
		}
		//Das Endergebnis aus dem Stack holen und als String zurück geben
		String result = ""+digits.pop();
		if(!digits.isEmpty() || !number.equals(""))
			throw new RuntimeException("Die Gleichung entspricht nicht der Postfixnotation");
		if(result.endsWith(".0"))
			result = result.substring(0,result.length()-2);
		return result;
	} //solve
	
	//Schreibt eine Zahl als Float oder Integer in den Stack
	private static void numberInStack(String number, boolean floatFound) {
		//Wenn die Zahl nur aus einem Punkt besteht wird dieser ignoriert
		if(number.startsWith(".") && number.length()==1) {
			number = "";
			return;
		}
		//Wenn die Zahl mit einem Punkt aufhört wird der Punkt abgeschnitten
		if(number.endsWith(".")) {
			number = number.substring(0,number.length()-1);
			floatFound = false;
		}
		digits.push(Float.parseFloat(number));
	} //numberInStack
}
Code:
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class UPNFrame extends JFrame implements ActionListener{

	private static final long serialVersionUID = -7501896257995843922L;
	
	private JPanel inputPanel, outputPanel;
	private JTextField input, output;
	private JLabel inputLabel, outputLabel;
	private JButton ok;
	
	public UPNFrame() {
		super("UPN-Rechner");
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		input = new JTextField(20);
		output = new JTextField(10);
		inputLabel = new JLabel("Eingabe: ");
		outputLabel = new JLabel("Ergebnis: ");
		ok = new JButton("Enter");
		
		inputPanel = new JPanel(new FlowLayout(FlowLayout.LEFT,10,5));
		inputPanel.add(inputLabel);
		inputPanel.add(input);
		inputPanel.add(ok);
		
		outputPanel = new JPanel(new FlowLayout(FlowLayout.LEFT,10,5));
		outputPanel.add(outputLabel,BorderLayout.NORTH);
		outputPanel.add(output,BorderLayout.SOUTH);
		
		ok.addActionListener(this);
		input.addActionListener(this);
		
		this.setLayout(new BorderLayout());
		this.add(inputPanel,BorderLayout.NORTH);
		this.add(outputPanel, BorderLayout.SOUTH);
		
		this.pack();
		this.setLocationRelativeTo(null);
		this.setVisible(true);
	} //ExtractFrame
	
	public void actionPerformed(ActionEvent e) {
		if(!input.getText().equals("")) {
			try {
				String result = UPNModul.solve(input.getText());
				output.setText(result);
			} catch(RuntimeException exc) {
				JOptionPane.showMessageDialog(this, exc.getMessage(), "Fehler", JOptionPane.ERROR_MESSAGE);
			}
			
		}	
	} //actionPerformed
	
	public static void main(String[] args) {
		new UPNFrame();
	} //main
}

Diesmal hab ich GUI und Programmlogik getrennt und mit Exceptions gearbeitet. :)
Im Anhang wieder die .jar-Datei zum testen.

Gruß,
Boar
 

Eydeet

Member
Meine Lösung entspricht zwar nicht ganz der geforderten Aufgabenstellung (Debug-Modus ist fest integriert), aber ich hoffe, das ist nicht so schlimm ;)

Um den Debug-Modus zu aktivieren, muss man die Option --debug anhängen (eigentlich ist egal, was man anhängt, es wird nur die Anzahl an Argumenten überprüft).
 

CDW

Moderator
Mitarbeiter
@Eydeet: Du hast da ein paar Fehler drin ;)
probier mal (im Debugmodus): 1 2+ 3 4+*
oder 5 6+ 7 8+*
(ohne das Leerzeichen zwischen +*)
oder 5 6+7 8+*

Es scheint irgendwie nicht möglich zu sein,
(5+6)*(7+8 ) zu berechnen
Wenn ich den Code richtig interpretiert habe: Du legst die Operatoren auf den Stack und rechnest nur, wenn ein NON-Operand/Operator kommt - das geht nicht gut ;)
 

Eydeet

Member
Ups :D Ich hab beim Testen immer normale Rechnungen wie 1 * 2 + 4 - 3 o.ä. eingegeben, weil mir die andere Form noch nie begegnet ist.

Jetzt also mit Postfix-Notation:

EDIT:
Ich hab noch mal eine verbesserte (?) Version angehängt (calc-1.1.zip).

In dieser Version sind Teile der Main-Funktion in eine weitere Klasse ausgelagert, und man kann direkt mehrere Rechnungen hintereinander eingeben, die sich aufeinander beziehen, da der alte Stack weiterverwendet wird.

Das macht es einem möglich, mit dem alten Ergebnis weiterzurechnen.
 

MontyPerl

New member
Hm, ich habs mal in perl gemacht, nimmt postfix sowie infix an. Bei postfix muss der Term komplett mit Leerzeichen getrennt sein, bei infix können die Klammern die Zahlen "berühren", aber Operatoren sollen immernoch "frei stehen". Und da immernur ein Argument pro "switch" erlaubt ist, muss man den Term auch quoten. (Auch um shell-Interpretation von Metazeichen entgegenzuwirken.)
Für Benutzungshilfe einfach den "-h" switch angeben.
Code:
#!/usr/bin/env perl
use warnings;
use strict;
use Getopt::Std;

# A little script for parsing mathematical expressions.

my $Operator = qr{[-+/%*x]};
my $Number = qr{[-+]?(:?(:?[1-9][0-9]*)?\.)?[1-9][0-9]*};
my $Bracket = qr{[)(]};


sub calcPostfix {
    my @numStack;
    foreach my $atom (@_) {
        if ($atom =~ m{^$Number$}) {push(@numStack, $atom)}
        elsif ($atom =~ m{^$Operator$}) {
            my ($b, $a) = (pop(@numStack), pop(@numStack));
            unless ($a) {$a = 0}
            unless ($b) {$b = 0}
            my $result;
            if    ($atom eq '*' or $atom eq 'x'){$result = $a * $b}
            elsif ($atom eq '/' or $atom eq '%'){$result = $a / $b}
            elsif ($atom eq '+')                {$result = $a + $b}
            elsif ($atom eq '-')                {$result = $a - $b}
            push(@numStack, $result)
        }
    }
    return pop(@numStack)
}

sub infix2postfix {
    my (@postfixTerm, @opStack);
    # A precendence-hash mapping operators to integers representing their precendence.
    my %precHash = ('*'   =>   2,
                    'x'   =>   2,
                    '%'   =>   2,
                    '/'   =>   2,
                    '+'   =>   1,
                    '-'   =>   1,
                    ')'   =>   0,
                    '('   =>   0,
                    'EOT' =>   0);

    push(@opStack, "EOT");
    PARSE: foreach my $atom (@_, "EOT") {       # EOT == "End of term" mark
        if    ($atom =~ m{^$Number$}) {push(@postfixTerm, $atom)}
        elsif ($atom =~ m{^$Operator$}) {
            if ($precHash{$atom} > $precHash{$opStack[$#opStack]}) {push(@opStack, $atom)}
            else {
                push(@postfixTerm, pop(@opStack));
                redo PARSE
            }
        }
        elsif ($atom =~ m{^$Bracket$}) {
            if ($atom eq '(') {push(@opStack, $atom)}
            else {
                while($opStack[$#opStack] ne '(') {push(@postfixTerm, pop(@opStack))}
                pop(@opStack)
            }
        }
        elsif ($atom eq "EOT") {
            push(@postfixTerm, pop(@opStack)) until ($opStack[$#opStack] eq "EOT")
        }
    }
    return @postfixTerm
}

sub Help {
    print "Usage: $0 [switch] [argument]\n\n";
    print "Available switches:\n";
    print "-h\t\tDisplay this help text and exit.\n";
    print "-i\t\tThis switch takes a term in infix notation as an argument.\n";
    print "-p\t\tThis switch takes a term in postfix notation as an argument.\n\n";
    print "Note: The postfix-notation has to be seperated by whitespaces.\n";
    print "This here for example:           $0 -p \'1 2 + 3 *\'\n";
    print "will work, while this here:      $0 -p \'1 2+ 3*\'\n";
    print "won\'t. Same goes for the infix notation, though the braces may\n";
    print "\"touch\" the digits like this:  $0 -i \'(1 + 2) * 3\'\n";
    exit(0)
}


unless (@ARGV) {Help()}

my %opts;
getopts('hi:p:', \%opts);

if (exists($opts{'h'})) {Help()}
print "<--Postfix Calculator---|\n";
print "                 \t|---------------------------------------------------------->\n";
if (exists($opts{'p'})) {
    print "Postfix notation:\t|\t$opts{'p'}\n";
    print "Result:          \t|\t".calcPostfix(split(/\s+/, $opts{'p'}))."\n";
    print "                 \t|---------------------------------------------------------->\n"
}
if (exists($opts{'i'})) {
    my @ifNotation = grep(/[^\s]+/, split(m{\s+|(?<=[\)-+/%*x0-9])(?=[)(])|(?<=[)(])(?=[\(-+/%*x1-9])}, $opts{'i'}));
    my @pfNotation = infix2postfix(@ifNotation);
    print "Infix notation:  \t|\t@ifNotation\n";
    print "Postfix notation:\t|\t@pfNotation\n";
    print "Result:          \t|\t".calcPostfix(@pfNotation)."\n";
    print "                 \t|---------------------------------------------------------->\n"
}
(Außerdem gehts von einem Unixoiden System aus (siehe shebang), also notfalls (der Fall in dem Windows installiert ist) einfach mit "perl skriptname" starten..)
 

v2.0

Stammuser
Java-Code

Hier ist dann noch meine Version in Java.
Das Programm bestimmt über die Ascii-Werte ob es sich um ein Leerzeichen handelt und um welchen Operator es sich handelt.

Code:
import javax.swing.JOptionPane;

public class Rechner {

    public static void main(String[] args) {
        String eingabe = JOptionPane.showInputDialog("Geben Sie zwei Operanden ein, die durch ein Leerzeichen voneinander getrennt sind und im Anschluss geben Sie einen Operator ein: ");
        StringBuffer operand1 = new StringBuffer();
        StringBuffer operand2 = new StringBuffer();
        int operator = 0;
        char zeichen;
        boolean einsoderzwei = true;
        int laenge = eingabe.length();
        for (int i = 0; i < laenge; i++) {
            zeichen = eingabe.charAt(i);
            int ascii = (int)zeichen;
            if (ascii == 32) { // 32 = Leerzeichen/Ascii
                einsoderzwei = false;
            }
            if (einsoderzwei == true) {
                operand1.append(zeichen);
            } else if ( einsoderzwei == false & ascii != 32 & ascii != 42 & ascii != 43 & ascii != 45 & ascii != 47){
                operand2.append(zeichen);
            } else if ( einsoderzwei == false & ascii == 42){ 
                operator = 1; // Multiplikation
            } else if ( einsoderzwei == false & ascii == 43){ 
                operator = 2; // Addition
            } else if ( einsoderzwei == false & ascii == 45){ 
                operator = 3; // Subtraktion
            } else if ( einsoderzwei == false & ascii == 47){ 
                operator = 4; // Division
            }
        }

        int zahl1, zahl2;
        String str1, str2;
        str1 = operand1.toString();
        zahl1 = Integer.parseInt(str1);
        str2 = operand2.toString();
        zahl2 = Integer.parseInt(str2);
        
        if (operator == 1) {
            JOptionPane.showMessageDialog(null, zahl1 + " * " + zahl2 +" = " + (zahl1/zahl2));
        } else if (operator == 2) {
            JOptionPane.showMessageDialog(null, zahl1 + " + " + zahl2 +" = " + (zahl1+zahl2));
        } else if (operator == 3) {
            JOptionPane.showMessageDialog(null, zahl1 + " - " + zahl2 +" = " + (zahl1-zahl2));
        } else if (operator == 4) {
            JOptionPane.showMessageDialog(null, zahl1 + " / " + zahl2 +" = " + (zahl1/zahl2));
        } else {
            JOptionPane.showMessageDialog(null, "Fehler!");
        }
    }

}
 
Oben