diff --git a/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java b/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java index fb18093..26cdc32 100644 --- a/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java +++ b/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java @@ -1,7 +1,9 @@ package com.mouseboy.assignment1; +import android.graphics.Typeface; import android.os.Bundle; import android.view.View; +import android.widget.TextView; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; @@ -9,6 +11,8 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import com.mouseboy.assignment1.parser.StateFarm; + import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -17,8 +21,7 @@ import java.util.ArrayList; public class MainCalculatorActivity extends AppCompatActivity { // I missing having decltype already - public static final ArrayList buttonIDs = new ArrayList<>(); - + private final StateFarm state = new StateFarm(this,""); @Override protected void onCreate(Bundle savedInstanceState) { @@ -31,31 +34,29 @@ public class MainCalculatorActivity extends AppCompatActivity { return insets; }); - // evil. - buttonIDs.add(R.id.b0); - buttonIDs.add(R.id.b1); - buttonIDs.add(R.id.b2); - buttonIDs.add(R.id.b3); - buttonIDs.add(R.id.b4); - buttonIDs.add(R.id.b5); - buttonIDs.add(R.id.b6); - buttonIDs.add(R.id.b7); - buttonIDs.add(R.id.b8); - buttonIDs.add(R.id.b9); + findViewById(R.id.b0).setOnClickListener((View view) -> state.addNumber("0")); + findViewById(R.id.b1).setOnClickListener((View view) -> state.addNumber("1")); + findViewById(R.id.b2).setOnClickListener((View view) -> state.addNumber("2")); + findViewById(R.id.b3).setOnClickListener((View view) -> state.addNumber("3")); + findViewById(R.id.b4).setOnClickListener((View view) -> state.addNumber("4")); + findViewById(R.id.b5).setOnClickListener((View view) -> state.addNumber("5")); + findViewById(R.id.b6).setOnClickListener((View view) -> state.addNumber("6")); + findViewById(R.id.b7).setOnClickListener((View view) -> state.addNumber("7")); + findViewById(R.id.b8).setOnClickListener((View view) -> state.addNumber("8")); + findViewById(R.id.b9).setOnClickListener((View view) -> state.addNumber("9")); - buttonIDs.add(R.id.bac); - buttonIDs.add(R.id.bc); + findViewById(R.id.bac).setOnClickListener((View view) -> state.clearAll()); + findViewById(R.id.bc).setOnClickListener((View view) -> state.clearCurrent()); - buttonIDs.add(R.id.bplus); - buttonIDs.add(R.id.bdiv); - buttonIDs.add(R.id.bminus); - buttonIDs.add(R.id.bmul); - - buttonIDs.add(R.id.bdot); - buttonIDs.add(R.id.bneg); - buttonIDs.add(R.id.bequals); - buttonIDs.add(R.id.bpar); + findViewById(R.id.bplus).setOnClickListener((View view) -> state.addOperator("+")); + findViewById(R.id.bdiv).setOnClickListener((View view) -> state.addOperator("÷")); + findViewById(R.id.bminus).setOnClickListener((View view) -> state.addOperator("-")); + findViewById(R.id.bmul).setOnClickListener((View view) -> state.addOperator("*")); + findViewById(R.id.bdot).setOnClickListener((View view) -> state.dot()); + findViewById(R.id.bneg).setOnClickListener((View view) -> state.neg()); + findViewById(R.id.bequals).setOnClickListener((View view) -> state.equals()); + findViewById(R.id.bpar).setOnClickListener((View view) -> state.paren()); } } \ No newline at end of file diff --git a/app/src/main/java/com/mouseboy/assignment1/helpers/Number.java b/app/src/main/java/com/mouseboy/assignment1/helpers/Number.java new file mode 100644 index 0000000..1d0fdaa --- /dev/null +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Number.java @@ -0,0 +1,11 @@ +package com.mouseboy.assignment1.helpers; + +public class Number implements Token { + + public final String value; + + public Number(String value) { + this.value = value; + } + +} diff --git a/app/src/main/java/com/mouseboy/assignment1/helpers/Operator.java b/app/src/main/java/com/mouseboy/assignment1/helpers/Operator.java new file mode 100644 index 0000000..2e93724 --- /dev/null +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Operator.java @@ -0,0 +1,10 @@ +package com.mouseboy.assignment1.helpers; + +public enum Operator implements Token { + Plus, + Minus, + Mul, + Div, + ParenLeft, + ParenRight, +} diff --git a/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java b/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java new file mode 100644 index 0000000..64767f0 --- /dev/null +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java @@ -0,0 +1,4 @@ +package com.mouseboy.assignment1.helpers; + +public interface Token { +} diff --git a/app/src/main/java/com/mouseboy/assignment1/parser/Evaluator.java b/app/src/main/java/com/mouseboy/assignment1/parser/Evaluator.java new file mode 100644 index 0000000..80361f5 --- /dev/null +++ b/app/src/main/java/com/mouseboy/assignment1/parser/Evaluator.java @@ -0,0 +1,11 @@ +package com.mouseboy.assignment1.parser; + +public class Evaluator { + + public final String value; + + Evaluator(String value) { + this.value = value; + } + +} diff --git a/app/src/main/java/com/mouseboy/assignment1/parser/StateFarm.java b/app/src/main/java/com/mouseboy/assignment1/parser/StateFarm.java new file mode 100644 index 0000000..3e77f35 --- /dev/null +++ b/app/src/main/java/com/mouseboy/assignment1/parser/StateFarm.java @@ -0,0 +1,350 @@ +package com.mouseboy.assignment1.parser; + +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.mouseboy.assignment1.R; +import com.mouseboy.assignment1.helpers.Number; +import com.mouseboy.assignment1.helpers.Operator; +import com.mouseboy.assignment1.helpers.Token; + +import java.util.ArrayList; + +public class StateFarm { + + private StringBuilder currentData = new StringBuilder(); + private final AppCompatActivity parent; + private final ArrayList tokens = new ArrayList<>(); + private int currentToken = 0; + + public StateFarm(AppCompatActivity parent, String restore) { + currentData.append(restore); + this.parent = parent; + } + + public void tokenize() { + currentToken = 0; + tokens.clear(); + for (int i = 0; i < currentData.length(); i++) { + char c = currentData.charAt(i); + switch (c) { + case '*': + tokens.add(Operator.Mul); + break; + case '+': + tokens.add(Operator.Plus); + break; + case '-': + tokens.add(Operator.Minus); + break; + case '÷': + tokens.add(Operator.Div); + break; + case '(': + tokens.add(Operator.ParenLeft); + break; + case ')': + tokens.add(Operator.ParenRight); + break; + default: + StringBuilder data = new StringBuilder(); + while (Character.isDigit(c) || c == '.') { + data.append(c); + if (++i >= currentData.length()) + break; + c = currentData.charAt(i); + } + --i; + tokens.add(new Number(data.toString())); + break; + } + } + } + + public Token peek() { + if (!hasNext()) + return null; + return tokens.get(currentToken); + } + + public void next() { + currentToken++; + } + + public boolean hasNext() { + return currentToken < tokens.size(); + } + + public Token last() { + return tokens.get(tokens.size() - 1); + } + + public String getString() { + return currentData.toString(); + } + + public void clearCurrent() { + if (currentData.length() == 0) + return; + currentData.deleteCharAt(currentData.length() - 1); + updateDisplay(); + } + + public void clearAll() { + currentData = new StringBuilder(); + updateDisplay(); + } + + public void addNumber(String number) { + tokenize(); + // adds a multiply for you if you add a number after a paren express () + if (!tokens.isEmpty() && last() == Operator.ParenRight) + currentData.append("*"); + currentData.append(number); + updateDisplay(); + } + + public void addOperator(String operator) { + tokenize(); + if (!tokens.isEmpty()) { + // changes operators if you haven't typed an expression + while (last() == Operator.ParenLeft) { + currentData.deleteCharAt(currentData.length() - 1); + tokens.remove(tokens.size() - 1); + } + // decimal needs to be completed (we will remove it for you :3) + if (last() instanceof Number) { + if (((Number) last()).value.endsWith(".")) + currentData.deleteCharAt(currentData.length() - 1); + // changes operators if you haven't typed an expression + } else if (last() == Operator.Div || last() == Operator.Mul + || last() == Operator.Minus || last() == Operator.Plus) { + currentData.deleteCharAt(currentData.length() - 1); + } + currentData.append(operator); + updateDisplay(); + } + } + + public void dot() { + tokenize(); + if (!tokens.isEmpty() && last() instanceof Number) { + Number v = (Number) last(); + if (!v.value.contains(".")) { + currentData.append('.'); + updateDisplay(); + } + } + } + + private void handleNegativeWithSubtract(int v, boolean matching) { + if (currentData.charAt(v) == '-') { + if (v == 0) + currentData.deleteCharAt(0); + else { + char c = currentData.charAt(v - 1); + if (!(Character.isDigit(c) || c == ')')) + currentData.deleteCharAt(v); + else + currentData.insert(v, '-'); + } + } else { + char c = currentData.charAt(v); + if (matching && c == '(' && currentData.charAt(v + 1) == '(') { + currentData.insert(v + 1, '-'); + } else { + if (!(Character.isDigit(c) || c == '(')) + currentData.insert(v + 1, '-'); + else + currentData.insert(v, '-'); + } + } + } + + public void neg() { + tokenize(); + if (tokens.isEmpty()) + return; + if (last() instanceof Number) { + int v = currentData.length() - 1; + while (v > 0 && + (Character.isDigit(currentData.charAt(v)) || currentData.charAt(v) == '.')) { + --v; + } + handleNegativeWithSubtract(v, false); + updateDisplay(); + } + if (last() == Operator.ParenRight) { + int outstanding = 0; + int v = currentData.length() - 1; + do { + if (currentData.charAt(v) == ')') + outstanding++; + else if (currentData.charAt(v) == '(') + outstanding--; + v--; + } while (v > 0 && outstanding != 0); + for (int i = v; i < currentData.length(); i++) + System.out.println(currentData.charAt(i)); + handleNegativeWithSubtract(v, outstanding == 0); + updateDisplay(); + } + } + + public void equals() { + double value = parse(); + currentData = new StringBuilder(); + currentData.append(value); + updateDisplay(); + } + + public void paren() { + // edge case for no input + if (currentData.length() == 0) { + currentData.append("("); + updateDisplay(); + return; + } + tokenize(); + // decimal needs to be completed ( we will remove it for you :3 ) + if (last() instanceof Number) { + if (((Number) last()).value.endsWith(".")) { + currentData.deleteCharAt(currentData.length() - 1); + tokenize(); + } + } + int outstanding_paren = 0; + for (Token op : tokens) { + if (op == Operator.ParenLeft) + outstanding_paren++; + else if (op == Operator.ParenRight) + outstanding_paren--; + } + if (last() instanceof Number) { + if (outstanding_paren == 0) + currentData.append("*("); + else + currentData.append(")"); + } else { + switch ((Operator) last()) { + case Plus: + case Minus: + case Mul: + case Div: + case ParenLeft: + currentData.append("("); + break; + case ParenRight: + if (outstanding_paren == 0) + currentData.append("*("); + else + currentData.append(")"); + break; + } + } + updateDisplay(); + } + + // BRING ON THE 2P05! + // Pseudo EBNF from what i can remember: + // Expr := Pres2 '+' Pres2 | Pres2 '-' Pres2 + // Pres2 := Pres3 '*' Pres3 | Pres3 '/' Pres3 + // Pres3 := '-'Pres3 | Number | '('Expr')' + // + // This implements operator precedence because the tree is expanding such that expressions + // are parsed from the bottom up. + // So Pres3 gets evaluated first followed by Pres2 followed by Expr (Pres1). + // I hope that makes sense? I wanted to evaluate this as an iterative stack machine + // (similar to my how GP library handles things https://github.com/Tri11Paragon/blt-gp) + // (https://github.com/Tri11Paragon/blt-gp/blob/094fa76b5823f81f653ef8b4065cd15d7501cfec/include/blt/gp/program.h#L143C1-L153C22) + // (yes i do realize this code ^ makes zero sense without a complex understanding of the library internals) + + public double parse() { + tokenize(); + return parse_pres_1(); + } + + private double parse_pres_1() { + double v = parse_pres_2(); + if (peek() == Operator.Plus) { + next(); + v += parse_pres_2(); + } else if (peek() == Operator.Minus) { + next(); + v -= parse_pres_2(); + } + return v; + } + + private double parse_pres_2() { + double v = parse_pres_3(); + if (peek() == Operator.Mul) { + next(); + v *= parse_pres_3(); + } else if (peek() == Operator.Div) { + next(); + double d = parse_pres_3(); + if (d == 0) + throw new ArithmeticException("Cannot divide by zero!"); + v /= d; + } + return v; + } + + private double parse_pres_3() { + if (peek() == Operator.ParenLeft) { + next(); // consume ( + double d = parse_pres_1(); // consume expression + next(); // consume ) + return d; + } else if (peek() == Operator.Minus) { + // negating a value + next(); + return -parse_pres_3(); + } else if (peek() instanceof Number) { + double value = Double.parseDouble(((Number) peek()).value); + next(); + return value; + } else { + throw new RuntimeException("There was an error parsing the expression!"); + } + } + + public void printAllTokens() { + for (Token t : tokens) + printToken(t); + } + + public void printToken(Token t) { + if (t instanceof Number) + System.out.println(((Number) t).value); + else { + switch ((Operator) t) { + case Plus: + System.out.println("+"); + break; + case Minus: + System.out.println("-"); + break; + case Mul: + System.out.println("*"); + break; + case Div: + System.out.println("/"); + break; + case ParenLeft: + System.out.println("("); + break; + case ParenRight: + System.out.println(")"); + break; + } + } + } + + public void updateDisplay() { + ((TextView) parent.findViewById(R.id.output)).setText(currentData.toString()); + } + +} diff --git a/app/src/main/res/layout-land/activity_main_calculator.xml b/app/src/main/res/layout-land/activity_main_calculator.xml index 8d72fa3..84e9b6c 100644 --- a/app/src/main/res/layout-land/activity_main_calculator.xml +++ b/app/src/main/res/layout-land/activity_main_calculator.xml @@ -49,13 +49,14 @@