import java.util.function.BiFunction;
/**
* Token class
* A Token is the key component of a mathematical expression
* - Operators. define the operator token, precedence, and mathematical calculation
* - Parenthesis. used to group terms
*/
public class Token {
private final Character token;
private final int precedence;
private final BiFunction<Double, Double, Double> calculation;
private final int numArgs;
// Constructor for passive token, used for non-operator tokens
public Token() {
this('0'); // telescope to next constructor
}
// Constructor for simple token, used for parenthesis
public Token(Character token) {
this(token,0,null,0); // telescope to next constructor
}
// Constructor for operators, contains precedence and calculation method
public Token(Character token, int precedence, BiFunction<Double, Double, Double> calculation, int numArgs) {
this.token = token;
this.precedence = precedence;
this.calculation = calculation;
this.numArgs = numArgs;
}
// Getter for token
public Character getToken() {
return token;
}
// Getter for precedence
public int getPrecedence() {
return precedence;
}
public int getNumArgs() {
return numArgs;
}
// Is precedent calculation
public boolean isPrecedent(Token token) {
return this.precedence > token.getPrecedence();
}
// Math calculation for operator and operands
public Double calculate(Double operand1, Double operand2) {
return this.calculation.apply(operand1, operand2);
}
// String return for token / operator
public String toString() {
return this.token.toString();
}
}
import java.util.function.BiFunction;
/**
* TermOrOperator class is a subclass of Token
* TermOrOperator is used to define the parts of a mathematical expression
* - Values. a string representation of the numerical value in the expression
* - Operators. define the operator token, precedence, and mathematical calculation
* - Parenthesis. used to group terms
*/
public class TermOrOperator extends Token {
private final String value;
// Constructor for values
public TermOrOperator(String value) {
this.value = value;
}
// Constructor for parenthesis
public TermOrOperator(Character token) {
super(token);
this.value = null;
}
// Constructor for operators
public TermOrOperator(Character token, int precedence, BiFunction<Double, Double, Double> calculation) {
super(token, precedence, calculation, 2);
this.value = null;
}
// Constructor for operators
public TermOrOperator(Character token, int precedence, BiFunction<Double, Double, Double> calculation, int numArgs) {
super(token, precedence, calculation, numArgs);
this.value = null;
}
// Get method for retrieving value
public String getValue() {
return value;
}
// toString method to return value according to type: value, operator, or parenthesis
public String toString() {
if (this.value == null) {
return super.toString();
}
return this.value;
}
}
import java.util.Map;
import java.util.function.BiFunction;
import java.util.HashMap;
/**
* Terms class is a collection of Term objects
* Terms are used to define the parts of a mathematical expression
* - Operators. define the operator, precedence, and mathematical calculation
* - Parenthesis. used to seperate and group terms
* - Space. Used to seperate terms
*
* A Map is choosen as the data structure because it enables fast lookups of Terms
*/
public class Tokens {
// Terms are stored in map, using Term token as the key
private Map<Character, TermOrOperator> map;
// Constructor initializes map
public Tokens() {
this.map = new HashMap<>();
}
// Put method for adding Parenthesis and Space
public void put(Character token) {
this.map.put(token, new TermOrOperator(token));
}
// Put method for adding Operators, precedence, and calculation
public void put(Character token, int precedence, BiFunction<Double, Double, Double> calculation, int numArgs) {
this.map.put(token, new TermOrOperator(token, precedence, calculation, numArgs));
}
// Put method for adding Operators, precedence, and calculation
public void put(Character token, int precedence, BiFunction<Double, Double, Double> calculation) {
this.map.put(token, new TermOrOperator(token, precedence, calculation));
}
// Get method for retrieving Term based on token lookup
public TermOrOperator get(Character token) {
return this.map.get(token);
}
// Get precedence method for retrieving precedence based on token lookup
public int getPrecedence(Character token) {
return this.map.get(token).getPrecedence();
}
// Contains method for checking if token exists in map
public boolean contains(Character token) {
return this.map.containsKey(token);
}
// toString method for converting entire map to string
public String toString() {
return this.map.toString();
}
}
import java.util.ArrayList;
import java.util.Stack;
/** In mathematics,
an expression or mathematical expression is a finite combination of symbols that is well-formed
according to rules that depend on the context.
In computers,
expression can be hard to calculate with precedence rules and user input errors
to handle computer math we often convert strings into reverse polish notation
*/
public class Calculator {
// Key instance variables
private final String expression;
private ArrayList<TermOrOperator> terms = new ArrayList<>();
private ArrayList<TermOrOperator> rpnTerms = new ArrayList<>();
private Tokens operators = new Tokens();
private Tokens seperators = new Tokens();
private Double result = 0.0;
/**
* Calculator constructor to create a calculation object for an expression
* This calculates the expression and saves the result and intermediate steps in instance variables
*
* @param expression
*/
public Calculator(String expression) {
// set up tokens used in an calculator
initOperators();
initSeperators();
// original expression
this.expression = expression;
// parse expression into individual terms
this.termTokenizer();
// place terms into polish notation
this.termsToPrefix();
// calculate polish notation expression into a result
this.prefixToResult();
}
/**
* Method to initialize operators, precedence, and calculation
* Fundamental to data structures is the ability to store and retrieve data quickly
* In this case, we are using a map to store operators and their precedence and calculation methods.
*
* Note, that through overloaded methods, we can store operators as Terms with different numbers of arguments
* This is specifically useful, in this case, for unary operators like square root
*/
private void initOperators() {
// Operators contain a token, precedence, and calculation using BiFunction
operators.put('*', 3, (a, b) -> a * b);
operators.put('/', 3, (a, b) -> a / b);
operators.put('%', 3, (a, b) -> a % b);
operators.put('+', 4, (a, b) -> a + b);
operators.put('-', 4, (a, b) -> a - b);
operators.put('^', 2, (a, b) -> Math.pow(a, b)); // Power operation
operators.put('√', 1, (a, b) -> Math.sqrt(a), 1); // Square root operation
}
/**
* Method to initialize seperators
* Seperators are used to seperate terms
* Additionally, the parenthesis are used to group terms and operations
*
* Note, that through overloaded methods, we can store seperators as Terms with different numbers of arguments
*/
private void initSeperators() {
// Seperators contain a token
seperators.put(' ');
seperators.put('(');
seperators.put(')');
}
/**
* Term Tokenizer takes original expression and converts it to ArrayList of mathematical terms and values
* Populates the this.terms instance of type ArrayList<TermOrOperator>
* In essence, this method tokenizes the expression into individual terms/cells in the ArrayList
*/
private void termTokenizer() {
int start = 0; // term split starting index
StringBuilder multiCharTerm = new StringBuilder(); // term holder
for (int i = 0; i < this.expression.length(); i++) {
Character c = this.expression.charAt(i);
if ( operators.contains(c) || seperators.contains(c) ) {
// 1st check for working term and add if it exists
if (multiCharTerm.length() > 0) {
this.terms.add(new TermOrOperator(this.expression.substring(start, i)));
}
// Add operator or parenthesis term to list
TermOrOperator t = operators.get(c);
if (t == null) {
t = seperators.get(c);
}
if (t != null && t.getToken() != ' ') {
this.terms.add(t);
}
// Get ready for next term
start = i + 1;
multiCharTerm = new StringBuilder();
} else {
// multi character terms: numbers, functions, perhaps non-supported elements
// Add next character to working term
multiCharTerm.append(c);
}
}
// Add last term
if (multiCharTerm.length() > 0) {
this.terms.add(new TermOrOperator(this.expression.substring(start)));
}
}
/**
* This method populates the this.rpnTerms instance of type ArrayList<TermOrOperator> from the this.terms
* Observe the inorder shift from before (terms) to after (termsToRPN) reorder
* This reordering is called Reverse Polish Notation (RPN)
* The terms are reordered by parenthesis and operator precedence, also called postfix notation
* RPN is commonly used in computer science to evaluate mathematical expressions
* RPN originated with with the Polish mathematician Jan Łukasiewicz
* RPN was later popularized by the Hewlett-Packard company in the 1970s with their scientific calculators
*
*/
private void termsToPrefix() {
// A stack is used to hold operators in the proper order for prefix notation
Stack<TermOrOperator> operatorStack = new Stack<>();
// Process each term (similar to termsToRPN but reversed)
for (TermOrOperator term : terms) {
if (term.getToken() == ')') { // close parenthesis
// Same logic as RPN, but reversed
while (operatorStack.peek() != null && operatorStack.peek().getToken() != '(') {
rpnTerms.add(0, operatorStack.pop()); // Push to the front for prefix order
}
operatorStack.pop(); // remove open parenthesis
} else if (term.getToken() == '(') { // open parenthesis
operatorStack.push(term);
} else if (operators.contains(term.getToken())) { // operator
// For prefix, we compare operator precedence and push onto stack accordingly
while (!operatorStack.isEmpty() && operators.contains(operatorStack.peek().getToken()) &&
term.isPrecedent(operatorStack.peek())) {
rpnTerms.add(0, operatorStack.pop());
}
operatorStack.push(term);
} else { // operand
rpnTerms.add(0, term); // Directly add operands to the front
}
}
// Empty the operator stack to the front for prefix order
while (!operatorStack.isEmpty()) {
rpnTerms.add(0, operatorStack.pop());
}
}
/**
* The method takes this.rpnTerms and calculates them into a final result
* Values proceed the operators, value(s) are popped from the this.rpmTerms and pushed onto the calcStack
* Operators are applied to the value(s) in the calcStack and the result is pushed back onto the calcStack
* The final result is the remaining value on the calcStack with the this.rpnTerms is empty
* The final result is stored in this.result
*
*/
private void prefixToResult() {
// Stack for operands
Stack<Double> calcStack = new Stack<>();
// Process the prefix expression from right to left
for (int i = rpnTerms.size() - 1; i >= 0; i--) {
TermOrOperator term = rpnTerms.get(i);
Double operand1 = 0.0, operand2 = 0.0, result = 0.0;
if (operators.contains(term.getToken())) {
if (term.getNumArgs() == 1) { // Unary operator
operand1 = calcStack.pop();
result = term.calculate(operand1, 0.0); // Pass a dummy 0.0 for second operand
} else { // Binary operator
operand1 = calcStack.pop();
operand2 = calcStack.pop();
result = term.calculate(operand1, operand2);
}
calcStack.push(result);
} else {
// Operand, push onto stack
calcStack.push(Double.parseDouble(term.toString()));
}
}
// Final result will be the last value on the stack
this.result = calcStack.pop();
}
// Print the expression, terms, and result from the instance variables
public String toString() {
return ("Original expression: " + this.expression + "\n" +
"Tokenized expression: " + this.terms.toString() + "\n" +
"Polish Notation: " +this.rpnTerms.toString() + "\n" +
"Final result: " + String.format("%.2f", this.result));
}
// Tester method contains a few examples of mathematical expressions
public static void main(String[] args) {
Calculator simpleMath = new Calculator("100 + 200 * 3");
System.out.println("Simple Math\n" + simpleMath);
System.out.println();
Calculator parenthesisMath = new Calculator("(100 + 200) * 3");
System.out.println("Parenthesis Math\n" + parenthesisMath);
System.out.println();
Calculator decimalMath = new Calculator("100.2 - 99.3");
System.out.println("Decimal Math\n" + decimalMath);
System.out.println();
Calculator moduloMath = new Calculator("300 % 200");
System.out.println("Modulo Math\n" + moduloMath);
System.out.println();
Calculator divisionMath = new Calculator("300/200");
System.out.println("Division Math\n" + divisionMath);
System.out.println();
Calculator pythagoreanMath = new Calculator("√(3^2 + 4^2)");
System.out.println("Pythagorean Theorem\n" + pythagoreanMath);
}
}
Calculator.main(null);
Simple Math
Original expression: 100 + 200 * 3
Tokenized expression: [100, +, 200, *, 3]
Polish Notation: [+, *, 3, 200, 100]
Final result: 700.00
Parenthesis Math
Original expression: (100 + 200) * 3
Tokenized expression: [(, 100, +, 200, ), *, 3]
Polish Notation: [*, 3, +, 200, 100]
Final result: 900.00
Decimal Math
Original expression: 100.2 - 99.3
Tokenized expression: [100.2, -, 99.3]
Polish Notation: [-, 99.3, 100.2]
Final result: -0.90
Modulo Math
Original expression: 300 % 200
Tokenized expression: [300, %, 200]
Polish Notation: [%, 200, 300]
Final result: 200.00
Division Math
Original expression: 300/200
Tokenized expression: [300, /, 200]
Polish Notation: [/, 200, 300]
Final result: 0.67
Pythagorean Theorem
Original expression: √(3^2 + 4^2)
Tokenized expression: [√, (, 3, ^, 2, +, 4, ^, 2, )]
Polish Notation: [√, +, ^, 2, 4, ^, 2, 3]
Final result: 4.90
Comments
You are seeing this because your Disqus shortname is not properly set. To configure Disqus, you should edit your
_config.yml
to include either adisqus.shortname
variable.If you do not wish to use Disqus, override the
comments.html
partial for this theme.