Mit Zahlen im String rechnen

Diskutiere Mit Zahlen im String rechnen im Java Basics - Anfänger-Themen Bereich.
L

Lasnik

Angenommen ich habe einen String, zum Beispiel "Solve: 5 + 7". Die beiden Zahlenwerte (5 und 7) können jedes Mal variieren, auch die Rechenoperation kann variieren (es könnte also genauso gut "Solve: 334 * 1000" heißen).
Wie würde ich jetzt diese beiden Werte aus dem String nehmen und mit ihnen rechnen.
Wenn es nicht allzu kompliziert ist, wäre mir am Liebsten noch weitere Zahlen und Rechenoperationen hinzuzufügen.

Ich denke mal ein sehr simples Problem, dennoch habe ich beim googeln leider nichts brauchbares gefunden.
 
F

fhoffmann

Ich denke mal ein sehr simples Problem
Das ist leider falsch. Das Problem ist ziemlich kompliziert. Du musst (zumindestens wenn du auch kompliziertere Formeln wie 27 * (5 + 4) betrachten willst, einen "Parser" schreiben. Dazu solltest du ein Buch über Compilerbau hinzuziehen.

Falls du wirklich nur einfache Operationen mit zwei Zahlen, zwischen denen ein Rechenzeichen steht, betrachten willst, kannst du den String mit einem regulären Ausdruck in seine drei (ohne das Solve:) Teile zerlegen und damit die Rechnung ausführen.
 
J

JennyL

Wie zerlege ich mir die Rechnung in einzelne Teile?
Das Rad muss doch nicht neu erfunden werden...
Code:
		<dependency>
			<groupId>org.mariuszgromada.math</groupId>
			<artifactId>MathParser.org-mXparser</artifactId>
			<version>4.4.2</version>
		</dependency>
Java:
import java.util.Scanner;

import org.mariuszgromada.math.mxparser.Expression;
import org.mariuszgromada.math.mxparser.parsertokens.Token;

public class ME1 {
	public static void main(String[] args) {
		System.out.println("Solve:");
		@SuppressWarnings("resource")
		String e = new Scanner(System.in).nextLine();
		Expression e2 = new Expression(e);
		int i = 0;
		double a = -1;
		double b = -1;
		char op = ' ';
		for (Token token : e2.getCopyOfInitialTokens()) {
			System.out.println(token.tokenStr);
			if (token.tokenTypeId == 0) {
				if (i == 0) {
					a = token.tokenValue;
				}
				if (i == 2) {
					b = token.tokenValue;
				}
			}
			if (token.tokenTypeId == 1) {
				op = token.tokenStr.charAt(0);
			}
			if (i == 2) {
				if (op == '+') {
					a += b;
				}
				// etc.
				i = 1;
			} else {
				i++;
			}
		}
		System.out.println(a);
		System.out.println("Proof: " + e2.calculate());
	}
}
Code:
Solve:
1+2+3
1
+
2
+
3
6.0
Proof: 6.0
 
T

TM69

Dann verwende doch die split-Funktion von String.

Code:
String text = "3 * 5";
text.split("*");
alternativ auch mit regex.
 
R

Rajmund

Formelparser zu schreiben ist meine Lieblingsbeschäftigung :)
Wenn du es selbst machen willst, hier ein paar Gedankenanstöße:

-zerlege den String in sinnvolle Einheiten (sog. Tokens), das sind: Zahlen, Operatoren, Klammern, evtl. Funktionsnamen. Packe sie auf einen Stapel.
-eine Formel ist immer eine Summe
-eine Summe besteht aus einem oder mehreren Summanden, zwischen denen ein + oder - ist
-ein Summand ist ein Produkt
-ein Produkt besteht aus einem oder mehreren Faktoren zwischen denen ein * oder / ist
-ein Faktor ist eine Potenz
-eine Potenz besteht aus einer Basis und ggf. einem oder mehreren Exponenten, zwischen denen ein ^ ist
-Basis und Exponenten sind Operanden
-ein Operand ist
eine Zahl oder
eine Summe, die in Klammern eingeschlossen ist oder
ein Funktionsname mit einer in Klammern eingeschlossenen Summe

Schreibe für Summe, Produkt, Potenz, Operand jeweils eine Methode.

Die Summe könnte so aussehen:
Code:
Methode Summe():
    ergebnis = Produkt()
    t = nächstesToken()
    solange t + oder - ist:
          summand = Produkt()
          wenn t +: ergebnis += summand
          wenn t -: ergebnis -= summand
          t = nächstesToken()
    lege t zurück auf den Stapel
    return ergebnis
So bzw. so ähnlich dann für Produkt und Potenz. Man kann das Konzept nach Belieben erweitern um boolesche Operatoren, verschiedene Datentypen etc. Grundgedanke dieses sog. Top-down-Parsers ist, dass mit den niederwertigsten Operationen begonnen wird (eine Summe ist niederwertiger als ein Produkt, denn „Punktrechnung geht vor Strichrechnung“)
Viel Spaß!
 
Zuletzt bearbeitet:
R

Rajmund

...das Zerlegen eines Strings in Tokens kann im einfachsten Fall mit text.split("\s+") geschehen, d.h. aufteilen an Leerzeichen. Dann muss aber bei den Formeln vor und nach jeder Zahl, jedem Operator, jeder Klammer, jedem Funktionsnamen ein Leerzeichen stehen.
 
T

temi

...das Zerlegen eines Strings in Tokens kann im einfachsten Fall mit text.split("\s+") geschehen, d.h. aufteilen an Leerzeichen. Dann muss aber bei den Formeln vor und nach jeder Zahl, jedem Operator, jeder Klammer, jedem Funktionsnamen ein Leerzeichen stehen.
Da es sich höchst wahrscheinlich um eine Übung handelt wird das wohl der gesuchte Weg sein. Anschließend kann man die beiden Strings mit den Ziffern in int umwandeln und den String mit dem Operator mittels if() oder switch() überprüfen und die Rechnung ausführen.
 
R

Rajmund

Anschließend kann man die beiden Strings mit den Ziffern in int umwandeln und den String mit dem Operator mittels if() oder switch() überprüfen und die Rechnung ausführen.
Man sollte die Dinge so einfach machen wie möglich, aber nicht einfacher (Albert Einstein) ;)
Der TE fragte nach einer Lösung, wie er das auch mit mehreren Zahlen und verschiedenen Rechenoperationen lösen kann. Wenn man einfach linear von vorn nach hinten durchgeht, ignoriert man die Wertigkeiten der Operatoren und bekommt falsche Ergebnisse. 2 + 3 * 4 sind eben 14 und nicht 20.
 
T

temi

Der TE fragte nach einer Lösung, wie er auch mit mehreren Zahlen und verschiedenen Rechenoperationen lösen kann.
Das lese ich anders.
Die beiden Zahlenwerte (5 und 7) können jedes Mal variieren, auch die Rechenoperation kann variieren (es könnte also genauso gut "Solve: 334 * 1000" heißen).
Das Muster ist meiner Leseweise nach immer: "Solve:" Leerzeichen Zahl1 Leerzeichen Operator Leerzeichen Zahl2

Und das lässt sich relativ einfach lösen. Wenn es nicht so sein sollte, dann wird es deutlich komplizierter.
 
R

Rajmund

Das lese ich anders.

Das Muster ist meiner Leseweise nach immer: "Solve:" Leerzeichen Zahl1 Leerzeichen Operator Leerzeichen Zahl2

Und das lässt sich relativ einfach lösen. Wenn es nicht so sein sollte, dann wird es deutlich komplizierter.
TE: "wäre mir am Liebsten noch weitere Zahlen und Rechenoperationen". Es wird komplizierter, aber wie man es lösen kann, hab ich ja grob beschrieben.
 
T

temi

TE: "wäre mir am Liebsten noch weitere Zahlen und Rechenoperationen". Es wird komplizierter, aber wie man es lösen kann, hab ich ja grob beschrieben.
Alter Fuchs ;) Man sollte einfach bis zum Ende lesen. Wann werde ich das endlich begreifen?

Dann wird es auf jeden Fall komplizierter.
 
J

JustNobody

Wenn man mit diesem Thema anfangen will, dann kann man es sich vielleicht auch leichter machen und mit einer Postfix Notation anfangen. Dann kann man die Klammern weglassen.

Dann hat man also Ausdrücke wie: 27 13 +, was (27 + 13) entspricht.
Oder 27 13 + 5 * -> ((27 + 13) * 5)

Parsen ist relativ trivial: Man nimmt ein Zeichen:
- Ist es ein Whitespace, dann ignoriert man es.
- Ist es eine Ziffer, dann wird diese Ziffer mit allen direkt nachfolgenden zu einer Zahl
- Ist es ein Operator, dann hat man den Operator direkt.

Wenn man dies dann auswerten will, dann geht das auch extrem einfach, da eben keine Regeln wie Punkt vor Strich Rechnung beachtet werden müssen:
Auswertung von Links nach Rechts
- Zahlen werden auf den Stack gelegt
- Operatoren nehmen sich die Operanden vom Stack, führen die Operation aus und legen das Ergebnis auf den Stack.
- Am Ende ist das Ergebnis auf dem Stack.
Ungültig war der Ausdruck, wenn ein Operator seine Operanden nicht bekommen konnte oder am Ende nicht genau eine Zahl auf dem Stack liegt.

Also bei 27 13 + 5 * sähe die Auswertung vor:
1. 27: Stack: 27; Rest: 13 + 5 *
2. 13: Stack: 13, 27; Rest: + 5 *
3. +: Stack: 40; Rest: 5 *
4. 5: Stack: 5, 40; Rest: *
5. *: Stack: 200; Rest:
==> Wir sind fertig und das Ergebnis ist 200.

Das war damals (vor 1990) bei uns im Unterricht die Einführung in die Auswertung.

Ich habe das damals dann noch vertieft mit dem Buch "Compilerbau" von Alfred v, Aho, Ravi Sethi und Jeffry D. Ullman (Übersetz von Prof. Dr. Gerhard Barth) aus dem Addison-Wesley Verlag. (Ist ein Zweiteiler). Das läuft (lief?) halt auch unter dem Spitznamen "Drachenbuch". Ist aber halt schon etwas älter - Das Original in englischer Sprache ist wohl von 86/87. Ich möchte also nicht ausschließen, dass es da inzwischen auch einiges an Neuerungen geben könnte. Aber das dürfte sich vor allem auf den zweiten Teil auswirken, wo es um Code Optimierungen und so geht. Und andere Bücher (neuere) Bücher kenne ich nicht, da ich diese Thematik nicht weiter vertieft habe in den letzten 20 Jahren....
==> Vorteil könnte sein, dass man das Buch ggf. gut ausleihen kann (zur Not Fernausleihe). Man muss also nicht einmal groß Geld ausgeben.

Ansonsten auch einfach einmal Google nutzen und nach "compilerbau drachenbuch pdf" oder so suchen. Da habe ich einige PDFs von Unis bekommen. Da werden bestimmt auch interessante Scripte dabei sein, die man durchsehen kann....
 
J

JennyL

Aber man kann doch als Übung mal versuchen, einen Text wie 3 * 5 auszuwerten
Schon richtig... Habe mal etwas mit den Grundrechenarten rumprobiert... Der mXparser dient hier nur für das Tokenizing und Leveln der Eingabe. Der Term muss (vollständig) geklammert sein:
Java:
import java.util.PriorityQueue;
import java.util.Scanner;

import org.mariuszgromada.math.mxparser.Expression;
import org.mariuszgromada.math.mxparser.parsertokens.Token;

public class ME1 {
	public static void main(String[] args) {
		// 1+((2*3)-4) == 3 
		// 1+(2*(3-4)) == -1
		// ((1+2)*3)-4 == 5
		System.out.println("Solve:");
		@SuppressWarnings("resource")
		String e = new Scanner(System.in).nextLine();
		Expression e2 = new Expression(e);
		PriorityQueue<Token2> queue = new PriorityQueue<Token2>();
		for (Token token : e2.getCopyOfInitialTokens()) {
			System.out.println(token.tokenStr + " (" + token.tokenLevel + ")");
			if (token.tokenTypeId == 0) {
				queue.add(new Token2(token));
			}
			if (token.tokenTypeId == 1) {
				queue.add(new Token2(token));
			}
		}
		Token2 t2 = queue.remove();
		double a = queue.remove().t.tokenValue;
		queue.add(t2);
		while (!queue.isEmpty()) {
			switch (queue.remove().t.tokenStr.charAt(0)) {
			case '+':
				a += queue.remove().t.tokenValue;
				break;
			case '-':
				a -= queue.remove().t.tokenValue;
				break;
			case '*':
				a *= queue.remove().t.tokenValue;
				break;
			case '/':
				a /= queue.remove().t.tokenValue;
				break;
			default:
				break;
			}
		}
		System.out.println(a);
		System.out.println("Proof: " + e2.calculate());
	}
}

class Token2 implements Comparable<Token2> {
	private static int level0 = 0;
	public Token t;
	private int level1;
	private int level2;

	public Token2(Token t) {
		this.t = t;
		this.level1 = t.tokenLevel;
		this.level2 = level0++;
	}

	public int compareTo(Token2 o) {
		if (level1 == o.level1)
			if (t.tokenTypeId == o.t.tokenTypeId)
				return level2 - o.level2;
			else
				return o.t.tokenTypeId - t.tokenTypeId;
		return o.level1 - level1;
	}
}
 
L

Lasnik

Ich hätte nie gedacht, dass es tatsächlich so kompliziert es mit Grundrechenarten zu hantieren :D
Vielen Dank für alle Antworten (übrigens habt ihr mir dabei nicht bei einer Übung oder Hausaufgabe geholfen, sondern bei meinem eigenen Projekt ;) )
 
R

Rajmund

Weil ich's so gerne mache, hier ein kleiner Formelparser, der Grundrechenarten, Klammern, ein paar Funktionen und Konstanten versteht. Das Zerlegen in Tokens ist etwas hingekliert, das kann man sicherlich schöner machen. Nicht so elegant ist freilich auch, dass jeder Operator 2x mit dem Token verglichen wird. Aber zum Verständnis langt das Beispiel allemal.

Code:
import java.util.Collections;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SimpleFormelParser {

    private Stack<String> tokens;
    
    // Zahlen, Woerter, einzelne Zeichen
    private final static Pattern pattern = Pattern.compile("(^[0-9\\.]+)|(^\\w+)|(^.)");
    
    private final static String ENDSYMBOL = "end of input";
    
    public SimpleFormelParser(String string) {
        tokens = new Stack<>();
        
        // den String zerlegen und in den Stack legen
        
        int cursor = 0;
        while (cursor < string.length()) {
            Matcher m = pattern.matcher(string.substring(cursor));
            if (m.find()) {
                String token = m.group().trim();
                if (!token.isEmpty()) {
                    tokens.push(token);
                }
                cursor += m.group().length();
            } else {
                throw new RuntimeException("Syntax error");
            }
        }
        
        Collections.reverse(tokens);
    }

    /**
     * Wertet die Formel aus.
     * @return
     */
    public double eval() {
        double result = Summe();
        if (!tokens.isEmpty()) {
            throw new RuntimeException(String.format("superfluous tokens: '%s'", tokens));
        }
        return result;
    }
    
    /**
     * Holt das naechste Token vom Stapel. Ist der Stapel leer, wird das Endsymbol geliefert.
     * @return
     */
    private String nextToken() {
        return tokens.isEmpty()?ENDSYMBOL:tokens.pop();
    }
    
    /**
     * Legt ein Token zurueck auf den Stapel, aber nicht, wenn es sich dabei um's Endsymbol handelt.
     * @param token
     */
    private void pushBack(String token) {
        if (token != ENDSYMBOL) {
            tokens.push(token);
        }
    }
    
    private double Summe() {
        double result = Produkt();

        String token = nextToken();
        while ("+".equals(token) || "-".equals(token)) {
            switch (token) {
            case "+": result += Produkt(); break;
            case "-": result -= Produkt(); break;
            }
            token = nextToken();
        }
        pushBack(token);
        
        return result;
    }
    
    private double Produkt() {
        double result = Potenz();
        
        String token = nextToken();
        while ("*".equals(token) || "/".equals(token)) {
            switch (token) {
            case "*": result *= Potenz(); break;
            case "/": result /= Potenz(); break;
            }
            token = nextToken();
        }
        pushBack(token);
        
        return result;
    }
    
    private double Potenz() {
        double result = Operand();
        
        String token = nextToken();
        while ("^".equals(token)) {
            result = Math.pow(result, Operand());
            token = nextToken();
        }
        pushBack(token);
        
        return result;
    }
    
    private double Operand() {
        String token = nextToken(); 

        switch (token) {
        // Klammerausdruecke
        case "(":
            double result = Summe();
            token = nextToken();
            if (!")".equals(token)) {
                throw new RuntimeException(String.format(") expected. Found '%s' instead.", token));
            }
            return result;
            
        // Funktionen. In der Mathematik wird das nachfolgende Produkt als Argument verstanden, es muss nicht geklammert sein
        case "sin" :  return Math.sin(Produkt());
        case "cos" :  return Math.cos(Produkt());
        case "sqrt":  return Math.sqrt(Produkt());
        
        // Konstanten
        case "pi"  :  return Math.PI;
        
        // Negation
        case "-"   :  return -Operand();
        
        // alles andere versuchen wir als Zahl zu interpretieren
        default:
            try {
                return Double.parseDouble(token);
            } catch (NumberFormatException e) {
                throw new RuntimeException(String.format("Invalid operand: '%s'", token));
            }
        }
    }
    
    public static void main(String [] args) {
        System.out.println(new SimpleFormelParser(" ( 1.7 * 10^2) * sin -1.6*pi").eval());
    }
}
 
R

Rajmund

Hier noch eine leicht kürzere Variante, die möglicherweise etwas schwieriger zu verstehen ist, aber erlaubt, unkompliziert Operatoren, Funktionen, Konstanten zu ergänzen, in dem man einen entsprechenden Eintrag in der Map macht.
Code:
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SimpleFormelParser {
    
    // Zahlen, Wörter, einzelne non-Whitespace-Zeichen. Alles ggf. mit führenden Leerzeichen
    private final static Pattern pattern = Pattern.compile("^\\s*([0-9\\.]+|\\w+|\\S)");
    private final static String ENDSYMBOL = "end of input";

    private Map<String,BiFunction<Double,Double,Double>> operators = new HashMap<>();
    private Map<String,Function<Double,Double>>          functions = new HashMap<>();
    private Map<String,Double>                           constants = new HashMap<>();
    
    private Stack<String> tokens = new Stack<>();
    
    public SimpleFormelParser(String string) {
        // den String zerlegen und in den Stack legen
        int cursor = 0;
        while (cursor < string.length()) {
            Matcher m = pattern.matcher(string.substring(cursor));
            if (m.find() && m.groupCount() > 0) {
                tokens.push(m.group(1));
                cursor += m.group().length();
            }  else {
                cursor++;
            }
        }

        tokens.push(ENDSYMBOL);
        Collections.reverse(tokens);

        // Hier Operatoren definieren. Die Zahl ist die Wertigkeit (je kleiner, desto höherwertiger, keine 0 verwenden). 'Punktrechnung geht vor Strichrechnung'
        operators.put("1^", (a,b) -> Math.pow(a,b));
        operators.put("2*", (a,b) -> a*b);
        operators.put("2/", (a,b) -> a/b);
        operators.put("3+", (a,b) -> a+b);
        operators.put("3-", (a,b) -> a-b);
        
        // Hier Funktionen definieren
        functions = new HashMap<>();
        functions.put("sin",  x -> Math.sin(x));
        functions.put("cos",  x -> Math.cos(x));
        functions.put("sqrt", x -> Math.sqrt(x));
        
        // Hier Konstanten definieren;
        constants.put("pi", Math.PI);
        constants.put("e",  Math.E);
    }

    /**
     * Wertet die Formel aus.
     */
    public double eval() {
        double result = Ausdruck(3);    // eine Formel ist ein Ausdruck 
        
        if (!tokens.isEmpty() && tokens.peek() != ENDSYMBOL) {
            throw new RuntimeException(String.format("superfluous tokens: '%s'", tokens));
        }
        return result;
    }
    
    /**
     * Parst einen mathematischen Ausdruck. Ein Ausdruck ist: <Ausdruck> <Operator> <Ausdruck> ... 
     * d.h., ein Ausdruck ist wiederum ein Ausdruck der Form <Ausdruck> <Operator> <Ausdruck> ... sofern die Hierarchy > 0 ist.<br>
     * Ist sie 0, wird mit der Methode Operand() ein Operand geparst.<br>
     * Entsprechend der Hierarchy wird in der Operator-Map nach passenden Operatoren gesucht.
     */
    private double Ausdruck(int hierarchy) {
        if (hierarchy <= 0) { 
            return Operand();
        }
        
        double result = Ausdruck(hierarchy-1);
        do {
            String token = tokens.pop();
            
            BiFunction<Double, Double, Double> operator = operators.get(String.format("%d%s", hierarchy, token));
            if (operator == null) {
                tokens.push(token);
                break;
            } else {
                result = operator.apply(result, Ausdruck(hierarchy-1));
            }

        } while (true);
        
        return result;
    }
    
    /**
     * Parst Negation, Klammerausdrücke, Funktionen, Konstanten, Zahlen
     */
    private double Operand() {
        String token = tokens.pop(); 

        switch (token) {
        case ENDSYMBOL: throw new RuntimeException("Unexpected end of input");
        
        // Negation
        case "-": return -Operand();
                
        // Klammerausdrücke
        case "(":
            double result = Ausdruck(3);
            token = tokens.pop();
            if (!")".equals(token)) {
                throw new RuntimeException(String.format(") expected. Found '%s' instead.", token));
            }
            return result;

        default:
            // Funktionen. In der Mathematik wird das nachfolgende Produkt als Argument verstanden, es muss nicht geklammert sein
            if (functions.containsKey(token)) {
                return functions.get(token).apply(Ausdruck(2));
            }
                
            // Konstanten
            if (constants.containsKey(token)) {
                return constants.get(token);
            }
            
            // alles andere versuchen wir als Zahl zu interpretieren
            try {
                return Double.parseDouble(token);
            } catch (NumberFormatException e) {
                throw new RuntimeException(String.format("Invalid operand: '%s'", token));
            }
        }
    }
    
    public static void main(String [] args) {
        System.out.println(new SimpleFormelParser(" ( 1.7 * 10^2) * sin -1.6*pi + 1+2*4*0.5").eval());
    }
}
 
J

JennyL

Ich hätte nie gedacht, dass es tatsächlich so kompliziert es mit Grundrechenarten zu hantieren :D
Vielen Dank für alle Antworten (übrigens habt ihr mir dabei nicht bei einer Übung oder Hausaufgabe geholfen, sondern bei meinem eigenen Projekt ;) )
also es ist schon richtig, das Thema ist nicht ganz trivial, sobald die Terme komplizierter werden... Zum Einlesen sei noch ans Herz gelegt: (geklammerte/ungeklammerte) infix, präfix und postfix Notation, Algo von Dijkstra (Shunting-yard algo), Regexes, Compilerbau, Lexikalische, Syntaktische und Semantische Analyse (Tokenizer, ...), kontextfreie Grammatik/Syntax, Syntaxbaum, Evaluierung.
 
R

Rajmund

also es ist schon richtig, das Thema ist nicht ganz trivial, sobald die Terme komplizierter werden... Zum Einlesen sei noch ans Herz gelegt: (geklammerte/ungeklammerte) infix, präfix und postfix Notation, Algo von Dijkstra (Shunting-yard algo), Regexes, Compilerbau, Lexikalische, Syntaktische und Semantische Analyse (Tokenizer, ...), kontextfreie Grammatik/Syntax, Syntaxbaum, Evaluierung.
Oder einfach mal mein Beispiel ausprobieren und mit einem Debugger nachvollziehen ;)
 
Thema: 

Mit Zahlen im String rechnen

Passende Stellenanzeigen aus deiner Region:
Anzeige

Neue Themen

Anzeige

Anzeige
Oben