Computer Science A
RPN Calculator - Interactive Lesson
- How Do Computers Solve Math?
- Step 1: The Token Class
- Challenge: Read the Token class below, then modify it to add a
POWERoperator check. - Code Runner Challenge
- Popcorn Hack 1: Precedence Check
- Challenge: Read the Token class below, then modify it to add a
- Step 2: TermOrOperator — Subclass of Token
- Challenge: Run the code to see how different TermOrOperator types behave.
- Code Runner Challenge
- Popcorn Hack 2: Inheritance & Polymorphism
- Step 3: Tokens Map — Fast Operator Lookup
- Challenge: Run the code and then add the
^(power) operator to the map. - Code Runner Challenge
- Popcorn Hack 3: HashMap Lookup
- Challenge: Run the code and then add the
- Step 4: Tokenizing an Expression
- Challenge: Run the tokenizer on different expressions to see how it breaks them down.
- Code Runner Challenge
- Trace It: How Does Tokenization Work?
- Character-by-Character Trace:
- Step 5: Converting to Reverse Polish Notation (The Stack Algorithm)
- The Shunting-Yard Algorithm (by Dijkstra)
- Challenge: Trace the algorithm by hand for
(100 + 200) * 3, then run to verify. - Code Runner Challenge
- RPN Ordering Challenge
- Available Tokens
- Your RPN Order
- Step 6: Evaluating RPN — The Final Calculation
- Example:
100 200 + 3 * - Interactive Stack Visualization
- Token Stream
- Stack
- Example:
- Challenge: Run the complete calculator, then try your own expressions!
- Code Runner Challenge
- Final Knowledge Check
How Do Computers Solve Math?
When you type (100 + 200) * 3 into a calculator, you instinctively know to add first, then multiply. But computers don’t think like humans — they need a systematic algorithm to handle:
- Operator precedence (
*before+) - Parentheses (grouping overrides precedence)
- Left-to-right evaluation
The solution? Reverse Polish Notation (RPN) — a technique invented by Polish mathematician Jan Łukasiewicz and popularized by Hewlett-Packard calculators in the 1970s.
The Pipeline
Expression String → Tokenize → Convert to RPN → Evaluate → Result
"(100 + 200) * 3" → [100, +, 200, *, 3] → [100, 200, +, 3, *] → 900
Key Data Structures
| Structure | Used For | Why |
|---|---|---|
| ArrayList | Storing tokens & RPN output | Ordered sequence, index access |
| Stack | Operator reordering & calculation | LIFO — last operator in is first out |
| HashMap | Looking up operators by character | O(1) lookup for precedence & calculation |
Let’s build each piece step by step.
Step 1: The Token Class
Every part of a math expression is a Token — a number, an operator (+, -, *, /), or a parenthesis.
The Token class stores:
- The character (e.g.
+,*) - The precedence (multiplication before addition)
- The calculation function (what the operator actually does)
Key Concept: We use Java’s BiFunction<Double, Double, Double> — a functional interface that takes two doubles and returns a double. This lets us store math operations as data.
Challenge: Read the Token class below, then modify it to add a POWER operator check.
Run the code to see the test output.
import java.util.function.BiFunction;
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/non-operator tokens
public Token() { this('0'); }
// Constructor for simple token (parenthesis)
public Token(Character token) { this(token, 0, null, 0); }
// Full constructor for operators
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;
}
public Character getToken() { return token; }
public int getPrecedence() { return precedence; }
public int getNumArgs() { return numArgs; }
// Returns true if THIS token has higher precedence (lower number = higher priority)
public boolean isPrecedent(Token other) {
return this.precedence > other.getPrecedence();
}
// Execute the math operation
public Double calculate(Double a, Double b) {
return this.calculation.apply(a, b);
}
public String toString() { return this.token.toString(); }
}
// --- TEST ---
Token add = new Token('+', 4, (a, b) -> a + b, 2);
Token mul = new Token('*', 3, (a, b) -> a * b, 2);
Token paren = new Token('(');
System.out.println("Token '+' precedence: " + add.getPrecedence());
System.out.println("Token '*' precedence: " + mul.getPrecedence());
System.out.println("Does '+' yield to '*'? " + add.isPrecedent(mul));
System.out.println("3 + 5 = " + add.calculate(3.0, 5.0));
System.out.println("3 * 5 = " + mul.calculate(3.0, 5.0));
// TODO: Create a POWER token ('^', precedence 2) and test 2^10
Code Runner Challenge
Read the Token class. It represents a single element in a math expression — an operator with its precedence and calculation function. The test creates a few tokens and checks precedence. Try modifying the test to create a POWER token with precedence 2.
Popcorn Hack 1: Precedence Check
In the Token class, lower precedence numbers mean higher priority. Given Token add = new Token('+', 4, ...) and Token mul = new Token('*', 3, ...), what does add.isPrecedent(mul) return?
Step 2: TermOrOperator — Subclass of Token
A TermOrOperator extends Token to also represent numeric values. This is an example of inheritance — the key AP CSA concept.
- If it’s a number: stores the value as a String (e.g.
"100","3.14") - If it’s an operator: inherits the token, precedence, and calculation from Token
- If it’s a parenthesis: just stores the character
Why inheritance? Both numbers and operators need to live in the same ArrayList. By making them share a parent class, we can store ArrayList<TermOrOperator> and treat them polymorphically.
Challenge: Run the code to see how different TermOrOperator types behave.
import java.util.function.BiFunction;
// --- Token (parent class) ---
class Token {
private final Character token;
private final int precedence;
private final BiFunction<Double, Double, Double> calculation;
private final int numArgs;
public Token() { this('0'); }
public Token(Character token) { this(token, 0, null, 0); }
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;
}
public Character getToken() { return token; }
public int getPrecedence() { return precedence; }
public int getNumArgs() { return numArgs; }
public boolean isPrecedent(Token t) { return this.precedence > t.getPrecedence(); }
public Double calculate(Double a, Double b) { return this.calculation.apply(a, b); }
public String toString() { return this.token.toString(); }
}
// --- TermOrOperator (child class) ---
class TermOrOperator extends Token {
private final String value;
// Constructor for numeric values
public TermOrOperator(String value) {
this.value = value;
}
// Constructor for parenthesis
public TermOrOperator(Character token) {
super(token);
this.value = null;
}
// Constructor for binary operators
public TermOrOperator(Character token, int precedence, BiFunction<Double, Double, Double> calculation) {
super(token, precedence, calculation, 2);
this.value = null;
}
// Constructor for unary operators (like sqrt)
public TermOrOperator(Character token, int precedence, BiFunction<Double, Double, Double> calculation, int numArgs) {
super(token, precedence, calculation, numArgs);
this.value = null;
}
public String getValue() { return value; }
public String toString() {
return (this.value != null) ? this.value : super.toString();
}
}
// --- TEST ---
TermOrOperator num = new TermOrOperator("42");
TermOrOperator plus = new TermOrOperator('+', 4, (a, b) -> a + b);
TermOrOperator openParen = new TermOrOperator('(');
System.out.println("Number token: " + num + " (value=" + num.getValue() + ")");
System.out.println("Operator token: " + plus + " (precedence=" + plus.getPrecedence() + ")");
System.out.println("Parenthesis token: " + openParen);
System.out.println();
System.out.println("Polymorphism demo — all in one ArrayList:");
java.util.ArrayList<TermOrOperator> list = new java.util.ArrayList<>();
list.add(openParen);
list.add(num);
list.add(plus);
list.add(new TermOrOperator("8"));
list.add(new TermOrOperator(')'));
System.out.println(list);
// TODO: Create a modulo operator (%, precedence 3) and test 10 % 3
Code Runner Challenge
TermOrOperator extends Token to also hold numeric values. Run the code to see the three types: value, operator, and parenthesis. Then try adding a new operator for modulo (%).
Popcorn Hack 2: Inheritance & Polymorphism
We store both numbers and operators in ArrayList<TermOrOperator>. Which AP CSA concept makes this possible?
Encapsulation — private fields hide implementation
Polymorphism — subclass objects stored as parent type
Abstraction — Token is an abstract class
Overloading — multiple constructors in Token
Step 3: Tokens Map — Fast Operator Lookup
The Tokens class uses a HashMap to store all operators and separators. This gives us O(1) lookup — when we encounter a character in the expression, we can instantly check:
- Is it an operator? →
operators.contains('+') - What’s its precedence? →
operators.getPrecedence('+') - How does it calculate? →
operators.get('+').calculate(3, 5)
AP CSA Connection: This is a real-world use of HashMap<K, V> — one of the most important data structures.
Challenge: Run the code and then add the ^ (power) operator to the map.
import java.util.Map;
import java.util.HashMap;
import java.util.function.BiFunction;
// Compact Token + TermOrOperator for this runner
class Token {
private final Character token;
private final int precedence;
private final BiFunction<Double, Double, Double> calculation;
private final int numArgs;
public Token() { this('0'); }
public Token(Character token) { this(token, 0, null, 0); }
public Token(Character token, int precedence, BiFunction<Double, Double, Double> calc, int numArgs) {
this.token = token; this.precedence = precedence; this.calculation = calc; this.numArgs = numArgs;
}
public Character getToken() { return token; }
public int getPrecedence() { return precedence; }
public int getNumArgs() { return numArgs; }
public boolean isPrecedent(Token t) { return this.precedence > t.getPrecedence(); }
public Double calculate(Double a, Double b) { return calculation.apply(a, b); }
public String toString() { return token.toString(); }
}
class TermOrOperator extends Token {
private final String value;
public TermOrOperator(String value) { this.value = value; }
public TermOrOperator(Character token) { super(token); this.value = null; }
public TermOrOperator(Character token, int prec, BiFunction<Double, Double, Double> calc) {
super(token, prec, calc, 2); this.value = null;
}
public TermOrOperator(Character token, int prec, BiFunction<Double, Double, Double> calc, int n) {
super(token, prec, calc, n); this.value = null;
}
public String getValue() { return value; }
public String toString() { return value != null ? value : super.toString(); }
}
// --- Tokens Map (HashMap wrapper) ---
class Tokens {
private Map<Character, TermOrOperator> map = new HashMap<>();
public void put(Character token) {
map.put(token, new TermOrOperator(token));
}
public void put(Character token, int prec, BiFunction<Double, Double, Double> calc) {
map.put(token, new TermOrOperator(token, prec, calc));
}
public void put(Character token, int prec, BiFunction<Double, Double, Double> calc, int n) {
map.put(token, new TermOrOperator(token, prec, calc, n));
}
public TermOrOperator get(Character token) { return map.get(token); }
public boolean contains(Character token) { return map.containsKey(token); }
public String toString() { return map.toString(); }
}
// --- TEST ---
Tokens ops = new Tokens();
ops.put('+', 4, (a, b) -> a + b);
ops.put('-', 4, (a, b) -> a - b);
ops.put('*', 3, (a, b) -> a * b);
ops.put('/', 3, (a, b) -> a / b);
ops.put('%', 3, (a, b) -> a % b);
System.out.println("Operator map: " + ops);
System.out.println("Contains '+'? " + ops.contains('+'));
System.out.println("Contains 'x'? " + ops.contains('x'));
System.out.println("Precedence of '+': " + ops.get('+').getPrecedence());
System.out.println("Precedence of '*': " + ops.get('*').getPrecedence());
System.out.println("10 / 3 = " + ops.get('/').calculate(10.0, 3.0));
System.out.println("10 % 3 = " + ops.get('%').calculate(10.0, 3.0));
// TODO: Add power operator (^, precedence 2, Math.pow) and test 2^8
Code Runner Challenge
The Tokens class uses a HashMap for O(1) operator lookup. Run to see all registered operators. Then add the power operator (^, precedence 2, Math.pow).
Popcorn Hack 3: HashMap Lookup
The Tokens class uses HashMap<Character, TermOrOperator>. What is the time complexity of checking operators.contains('+')?
Step 4: Tokenizing an Expression
Before we can do any math, we need to break the expression string into tokens. This is called tokenization — the same concept compilers use to parse source code.
"(100 + 200) * 3" → [(, 100, +, 200, ), *, 3]
The algorithm walks character by character:
- If it’s a digit or decimal point → accumulate into a multi-character number
- If it’s an operator or separator → flush the accumulated number, then add the operator
- Spaces are separators but don’t get added as tokens
Challenge: Run the tokenizer on different expressions to see how it breaks them down.
import java.util.*;
import java.util.function.BiFunction;
class Token {
private final Character token; private final int precedence;
private final BiFunction<Double, Double, Double> calc; private final int numArgs;
public Token() { this('0'); }
public Token(Character t) { this(t,0,null,0); }
public Token(Character t, int p, BiFunction<Double,Double,Double> c, int n) { token=t; precedence=p; calc=c; numArgs=n; }
public Character getToken() { return token; }
public int getPrecedence() { return precedence; }
public int getNumArgs() { return numArgs; }
public boolean isPrecedent(Token t) { return precedence > t.getPrecedence(); }
public Double calculate(Double a, Double b) { return calc.apply(a,b); }
public String toString() { return token.toString(); }
}
class TermOrOperator extends Token {
private final String value;
public TermOrOperator(String v) { value=v; }
public TermOrOperator(Character t) { super(t); value=null; }
public TermOrOperator(Character t, int p, BiFunction<Double,Double,Double> c) { super(t,p,c,2); value=null; }
public TermOrOperator(Character t, int p, BiFunction<Double,Double,Double> c, int n) { super(t,p,c,n); value=null; }
public String getValue() { return value; }
public String toString() { return value!=null ? value : super.toString(); }
}
class Tokens {
private Map<Character,TermOrOperator> map = new HashMap<>();
public void put(Character t) { map.put(t, new TermOrOperator(t)); }
public void put(Character t, int p, BiFunction<Double,Double,Double> c) { map.put(t, new TermOrOperator(t,p,c)); }
public void put(Character t, int p, BiFunction<Double,Double,Double> c, int n) { map.put(t, new TermOrOperator(t,p,c,n)); }
public TermOrOperator get(Character t) { return map.get(t); }
public boolean contains(Character t) { return map.containsKey(t); }
}
// --- Tokenizer ---
Tokens operators = new Tokens();
operators.put('+', 4, (a,b) -> a+b);
operators.put('-', 4, (a,b) -> a-b);
operators.put('*', 3, (a,b) -> a*b);
operators.put('/', 3, (a,b) -> a/b);
operators.put('%', 3, (a,b) -> a%b);
operators.put('^', 2, (a,b) -> Math.pow(a,b));
Tokens separators = new Tokens();
separators.put(' ');
separators.put('(');
separators.put(')');
ArrayList<TermOrOperator> tokenize(String expr, Tokens ops, Tokens seps) {
ArrayList<TermOrOperator> terms = new ArrayList<>();
int start = 0;
StringBuilder multi = new StringBuilder();
for (int i = 0; i < expr.length(); i++) {
char c = expr.charAt(i);
if (ops.contains(c) || seps.contains(c)) {
if (multi.length() > 0) {
terms.add(new TermOrOperator(expr.substring(start, i)));
}
TermOrOperator t = ops.get(c);
if (t == null) t = seps.get(c);
if (t != null && t.getToken() != ' ') terms.add(t);
start = i + 1;
multi = new StringBuilder();
} else {
multi.append(c);
}
}
if (multi.length() > 0) terms.add(new TermOrOperator(expr.substring(start)));
return terms;
}
// --- TEST ---
String[] expressions = {
"100 + 200 * 3",
"(100 + 200) * 3",
"3.14 * 2",
"2 ^ 10",
"100 % 7"
};
for (String expr : expressions) {
System.out.println("\"" + expr + "\" → " + tokenize(expr, operators, separators));
}
// TODO: Try tokenizing your own expression!
Code Runner Challenge
The tokenizer splits a math expression string into individual tokens. Run it to see how different expressions get tokenized. Try adding your own expression at the bottom.
Trace It: How Does Tokenization Work?
"(100 + 200) * 3"
Walk through each character. What happens at each step?
Character-by-Character Trace:
multi is empty, skip number flush — Add ( token — Result: [(]multi = "100" — No token added yetmulti — Add number token "100" — Space is not added (filtered out) — Result: [(, 100]multi is empty — Add + operator token — Result: [(, 100, +]"200""200" — Add ) token — Result: [(, 100, +, 200, )]* operator added, then space, then "3" accumulated and flushed at end-of-string[(, 100, +, 200, ), *, 3] — 7 tokens from a 15-character string. Notice how spaces are used as delimiters but never become tokens themselves.Step 5: Converting to Reverse Polish Notation (The Stack Algorithm)
This is the core algorithm. RPN reorders tokens so that operators come AFTER their operands, eliminating the need for parentheses.
Infix (normal): (100 + 200) * 3
RPN (postfix): 100 200 + 3 *
The Shunting-Yard Algorithm (by Dijkstra)
Uses a Stack to reorder operators:
- Number → goes straight to output
(→ push onto operator stack)→ pop operators to output until(is found- Operator → pop higher-precedence operators from stack to output, then push current
- End → pop remaining operators to output
Challenge: Trace the algorithm by hand for (100 + 200) * 3, then run to verify.
import java.util.*;
import java.util.function.BiFunction;
class Token {
private final Character token; private final int precedence;
private final BiFunction<Double,Double,Double> calc; private final int numArgs;
public Token() { this('0'); }
public Token(Character t) { this(t,0,null,0); }
public Token(Character t,int p,BiFunction<Double,Double,Double> c,int n) { token=t;precedence=p;calc=c;numArgs=n; }
public Character getToken() { return token; }
public int getPrecedence() { return precedence; }
public int getNumArgs() { return numArgs; }
public boolean isPrecedent(Token t) { return precedence>t.getPrecedence(); }
public Double calculate(Double a,Double b) { return calc.apply(a,b); }
public String toString() { return token.toString(); }
}
class TermOrOperator extends Token {
private final String value;
public TermOrOperator(String v) { value=v; }
public TermOrOperator(Character t) { super(t); value=null; }
public TermOrOperator(Character t,int p,BiFunction<Double,Double,Double> c) { super(t,p,c,2); value=null; }
public TermOrOperator(Character t,int p,BiFunction<Double,Double,Double> c,int n) { super(t,p,c,n); value=null; }
public String getValue() { return value; }
public String toString() { return value!=null?value:super.toString(); }
}
class Tokens {
Map<Character,TermOrOperator> map=new HashMap<>();
void put(Character t) { map.put(t,new TermOrOperator(t)); }
void put(Character t,int p,BiFunction<Double,Double,Double> c) { map.put(t,new TermOrOperator(t,p,c)); }
TermOrOperator get(Character t) { return map.get(t); }
boolean contains(Character t) { return map.containsKey(t); }
}
Tokens ops = new Tokens();
ops.put('+',4,(a,b)->a+b); ops.put('-',4,(a,b)->a-b);
ops.put('*',3,(a,b)->a*b); ops.put('/',3,(a,b)->a/b);
ops.put('^',2,(a,b)->Math.pow(a,b));
Tokens seps = new Tokens();
seps.put(' '); seps.put('('); seps.put(')');
ArrayList<TermOrOperator> tokenize(String expr) {
ArrayList<TermOrOperator> terms = new ArrayList<>();
int start=0; StringBuilder multi=new StringBuilder();
for (int i=0;i<expr.length();i++) {
char c=expr.charAt(i);
if (ops.contains(c)||seps.contains(c)) {
if (multi.length()>0) terms.add(new TermOrOperator(expr.substring(start,i)));
TermOrOperator t=ops.get(c); if(t==null) t=seps.get(c);
if (t!=null && t.getToken()!=' ') terms.add(t);
start=i+1; multi=new StringBuilder();
} else multi.append(c);
}
if (multi.length()>0) terms.add(new TermOrOperator(expr.substring(start)));
return terms;
}
// --- Shunting-Yard: Infix → RPN ---
ArrayList<TermOrOperator> toRPN(ArrayList<TermOrOperator> terms) {
ArrayList<TermOrOperator> rpn = new ArrayList<>();
Stack<TermOrOperator> opStack = new Stack<>();
for (TermOrOperator term : terms) {
if (term.getToken() == '(') {
opStack.push(term);
} else if (term.getToken() == ')') {
while (opStack.peek().getToken() != '(') rpn.add(opStack.pop());
opStack.pop(); // remove '('
} else if (ops.contains(term.getToken())) {
while (!opStack.isEmpty() && ops.contains(opStack.peek().getToken()) && term.isPrecedent(opStack.peek()))
rpn.add(opStack.pop());
opStack.push(term);
} else {
rpn.add(term);
}
}
while (!opStack.isEmpty()) rpn.add(opStack.pop());
return rpn;
}
// --- TEST ---
String[] expressions = {
"100 + 200 * 3",
"(100 + 200) * 3",
"3 + 4 * 2 - 1",
"2 ^ 10"
};
for (String expr : expressions) {
ArrayList<TermOrOperator> tokens = tokenize(expr);
ArrayList<TermOrOperator> rpn = toRPN(tokens);
System.out.println("Infix: " + expr);
System.out.println("Tokens: " + tokens);
System.out.println("RPN: " + rpn);
System.out.println();
}
Code Runner Challenge
The Shunting-Yard algorithm converts infix to RPN using a Stack. Run to see step-by-step conversion. Try tracing "5 + 3 * 2" by hand first, then verify with the code.
RPN Ordering Challenge
Drag the tokens into correct Reverse Polish Notation order for: (5 + 3) * 2
Available Tokens
Your RPN Order
Step 6: Evaluating RPN — The Final Calculation
Once we have RPN, evaluation is beautifully simple using a Stack:
- Read tokens left to right
- Number → push onto stack
- Operator → pop operand(s), calculate, push result back
- Done → one number remains on stack = final answer
Example: 100 200 + 3 *
| Step | Token | Stack | Action |
|---|---|---|---|
| 1 | 100 | [100] | Push |
| 2 | 200 | [100, 200] | Push |
| 3 | + | [300] | Pop 200 & 100, add, push 300 |
| 4 | 3 | [300, 3] | Push |
| 5 | * | [900] | Pop 3 & 300, multiply, push 900 |
Result: 900
Interactive Stack Visualization
Step through RPN evaluation of 100 200 + 3 * and watch the stack in action.
Token Stream
Stack
Step 7: The Complete Calculator
Now let’s put it all together. The full Calculator class chains the entire pipeline:
Expression → Tokenize → RPN → Evaluate → Result
Challenge: Run the complete calculator, then try your own expressions!
Supported operators: +, -, *, /, %, ^ (power), √ (square root)
import java.util.*;
import java.util.function.BiFunction;
class Token {
private final Character token; private final int precedence;
private final BiFunction<Double,Double,Double> calc; private final int numArgs;
public Token() { this('0'); }
public Token(Character t) { this(t,0,null,0); }
public Token(Character t,int p,BiFunction<Double,Double,Double> c,int n) {
token=t; precedence=p; calc=c; numArgs=n;
}
public Character getToken() { return token; }
public int getPrecedence() { return precedence; }
public int getNumArgs() { return numArgs; }
public boolean isPrecedent(Token t) { return precedence > t.getPrecedence(); }
public Double calculate(Double a, Double b) { return calc.apply(a,b); }
public String toString() { return token.toString(); }
}
class TermOrOperator extends Token {
private final String value;
public TermOrOperator(String v) { value=v; }
public TermOrOperator(Character t) { super(t); value=null; }
public TermOrOperator(Character t,int p,BiFunction<Double,Double,Double> c) { super(t,p,c,2); value=null; }
public TermOrOperator(Character t,int p,BiFunction<Double,Double,Double> c,int n) { super(t,p,c,n); value=null; }
public String getValue() { return value; }
public String toString() { return value!=null ? value : super.toString(); }
}
class Tokens {
private Map<Character,TermOrOperator> map = new HashMap<>();
public void put(Character t) { map.put(t, new TermOrOperator(t)); }
public void put(Character t,int p,BiFunction<Double,Double,Double> c) { map.put(t, new TermOrOperator(t,p,c)); }
public void put(Character t,int p,BiFunction<Double,Double,Double> c,int n) { map.put(t, new TermOrOperator(t,p,c,n)); }
public TermOrOperator get(Character t) { return map.get(t); }
public int getPrecedence(Character t) { return map.get(t).getPrecedence(); }
public boolean contains(Character t) { return map.containsKey(t); }
}
public class Calculator {
private final String expression;
private ArrayList<TermOrOperator> terms = new ArrayList<>();
private ArrayList<TermOrOperator> rpnTerms = new ArrayList<>();
private Tokens operators = new Tokens();
private Tokens separators = new Tokens();
private Double result = 0.0;
public Calculator(String expression) {
initOperators();
initSeparators();
this.expression = expression;
this.termTokenizer();
this.termsToRPN();
this.rpnToResult();
}
private void initOperators() {
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));
}
private void initSeparators() {
separators.put(' ');
separators.put('(');
separators.put(')');
}
private void termTokenizer() {
int start = 0;
StringBuilder multi = new StringBuilder();
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (operators.contains(c) || separators.contains(c)) {
if (multi.length() > 0)
terms.add(new TermOrOperator(expression.substring(start, i)));
TermOrOperator t = operators.get(c);
if (t == null) t = separators.get(c);
if (t != null && t.getToken() != ' ') terms.add(t);
start = i + 1;
multi = new StringBuilder();
} else {
multi.append(c);
}
}
if (multi.length() > 0) terms.add(new TermOrOperator(expression.substring(start)));
}
private void termsToRPN() {
Stack<TermOrOperator> opStack = new Stack<>();
for (TermOrOperator term : terms) {
if (term.getToken() == '(') {
opStack.push(term);
} else if (term.getToken() == ')') {
while (opStack.peek().getToken() != '(') rpnTerms.add(opStack.pop());
opStack.pop();
} else if (operators.contains(term.getToken())) {
while (!opStack.isEmpty() && operators.contains(opStack.peek().getToken())
&& term.isPrecedent(opStack.peek()))
rpnTerms.add(opStack.pop());
opStack.push(term);
} else {
rpnTerms.add(term);
}
}
while (!opStack.isEmpty()) rpnTerms.add(opStack.pop());
}
private void rpnToResult() {
Stack<Double> calcStack = new Stack<>();
for (TermOrOperator term : rpnTerms) {
if (operators.contains(term.getToken())) {
Double op2 = calcStack.pop();
Double op1 = calcStack.pop();
calcStack.push(term.calculate(op1, op2));
} else {
calcStack.push(Double.valueOf(term.getValue()));
}
}
this.result = calcStack.pop();
}
public String toString() {
return "Original expression: " + expression + "\n"
+ "Tokenized expression: " + terms + "\n"
+ "Reverse Polish Notation: " + rpnTerms + "\n"
+ "Final result: " + String.format("%.2f", result);
}
public static void main(String[] args) {
System.out.println("=== Simple Math ===");
System.out.println(new Calculator("100 + 200 * 3"));
System.out.println();
System.out.println("=== Parentheses Override Precedence ===");
System.out.println(new Calculator("(100 + 200) * 3"));
System.out.println();
System.out.println("=== Decimals ===");
System.out.println(new Calculator("100.2 - 99.3"));
System.out.println();
System.out.println("=== Modulo ===");
System.out.println(new Calculator("300 % 200"));
System.out.println();
System.out.println("=== Power ===");
System.out.println(new Calculator("2 ^ 10"));
System.out.println();
// TODO: Add your own expression here!
// System.out.println(new Calculator("YOUR EXPRESSION"));
}
}
Calculator.main(null);
Code Runner Challenge
The complete RPN Calculator! Run it to see the full pipeline — from expression to tokenization to RPN to result. Try adding your own expressions at the bottom of main().
Knowledge Check
Test your understanding of the full RPN Calculator pipeline!
Final Knowledge Check
5 questions covering the entire lesson
3 + 4 * 2?BiFunction<Double, Double, Double> is used for the calculation field. What type of Java feature is BiFunction?5 3 + 2 *, what is on the stack after processing the + token?) character, what does it do?Key Takeaways
Data Structures Used
| Data Structure | Where | Purpose |
|---|---|---|
| ArrayList | terms, rpnTerms |
Ordered storage of tokens |
| Stack | Shunting-yard & evaluation | LIFO for operator reordering and calculation |
| HashMap | Tokens class |
O(1) lookup of operators by character |
AP CSA Concepts Demonstrated
- Inheritance —
TermOrOperator extends Token - Polymorphism — numbers and operators stored in the same
ArrayList<TermOrOperator> - Encapsulation — private fields with getters
- Functional Interfaces —
BiFunction<Double, Double, Double>for operator calculations - Algorithmic Thinking — Dijkstra’s Shunting-Yard algorithm
Hack Ideas
- Add support for unary minus (negative numbers like
-5 + 3) - Add trigonometric functions (
sin,cos,tan) - Add error handling for malformed expressions (mismatched parentheses, division by zero)
- Build a visual step-by-step debugger that shows the stack state at each step
- Create a GUI calculator that uses this engine under the hood