diff --git a/ReadMe.odt b/ReadMe.odt new file mode 100644 index 0000000..1e82a87 Binary files /dev/null and b/ReadMe.odt differ diff --git a/ReadMe.pdf b/ReadMe.pdf new file mode 100644 index 0000000..f4322fb Binary files /dev/null and b/ReadMe.pdf differ diff --git a/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java b/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java index 26cdc32..a5ed9e3 100644 --- a/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java +++ b/app/src/main/java/com/mouseboy/assignment1/MainCalculatorActivity.java @@ -2,10 +2,12 @@ package com.mouseboy.assignment1; import android.graphics.Typeface; import android.os.Bundle; +import android.os.PersistableBundle; import android.view.View; import android.widget.TextView; import androidx.activity.EdgeToEdge; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; @@ -21,7 +23,7 @@ import java.util.ArrayList; public class MainCalculatorActivity extends AppCompatActivity { // I missing having decltype already - private final StateFarm state = new StateFarm(this,""); + private StateFarm state = new StateFarm(this,""); @Override protected void onCreate(Bundle savedInstanceState) { @@ -34,6 +36,7 @@ public class MainCalculatorActivity extends AppCompatActivity { return insets; }); + // setup button listeners 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")); @@ -59,4 +62,17 @@ public class MainCalculatorActivity extends AppCompatActivity { findViewById(R.id.bpar).setOnClickListener((View view) -> state.paren()); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + // only equation string needs to be saved / restored + outState.putString("equ", state.getString()); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + state = new StateFarm(this, savedInstanceState.getString("equ")); + state.updateDisplay(); + } } \ 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 index 1d0fdaa..0226ff3 100644 --- a/app/src/main/java/com/mouseboy/assignment1/helpers/Number.java +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Number.java @@ -1,5 +1,6 @@ package com.mouseboy.assignment1.helpers; +// numbers are special tokens which store data. It is annoying you can't store data inside enums public class Number implements Token { public final String 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 index 2e93724..b3acdbe 100644 --- a/app/src/main/java/com/mouseboy/assignment1/helpers/Operator.java +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Operator.java @@ -1,5 +1,6 @@ package com.mouseboy.assignment1.helpers; +// thanks to my bf for telling me you can implement an interface. I hate it. public enum Operator implements Token { Plus, Minus, diff --git a/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java b/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java index 64767f0..b1c069c 100644 --- a/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Token.java @@ -1,4 +1,5 @@ package com.mouseboy.assignment1.helpers; +// empty interface representing a token for the tokenizer / parser public interface Token { } diff --git a/app/src/main/java/com/mouseboy/assignment1/helpers/Utils.java b/app/src/main/java/com/mouseboy/assignment1/helpers/Utils.java index fd59e1f..f35566b 100644 --- a/app/src/main/java/com/mouseboy/assignment1/helpers/Utils.java +++ b/app/src/main/java/com/mouseboy/assignment1/helpers/Utils.java @@ -7,26 +7,17 @@ import java.text.DecimalFormatSymbols; public class Utils { // why does java not have a nice decimal formatter + // function makes decimals nice, only displays 2 digits of precision public static String formatDecimal(double value) { BigDecimal decimalValue = BigDecimal.valueOf(value); String pattern; - - if (decimalValue.stripTrailingZeros().scale() <= 0) { + if (decimalValue.stripTrailingZeros().scale() <= 0) pattern = "#,##0"; - } else { + else pattern = "#,##0.##"; - } - DecimalFormat df = new DecimalFormat(pattern); - - DecimalFormatSymbols symbols = new DecimalFormatSymbols(); - symbols.setDecimalSeparator('.'); - symbols.setGroupingSeparator(','); - - df.setDecimalFormatSymbols(symbols); - - return df.format(value); + return new DecimalFormat(pattern).format(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 index 3e77f35..3ff51bb 100644 --- a/app/src/main/java/com/mouseboy/assignment1/parser/StateFarm.java +++ b/app/src/main/java/com/mouseboy/assignment1/parser/StateFarm.java @@ -8,11 +8,14 @@ import com.mouseboy.assignment1.R; import com.mouseboy.assignment1.helpers.Number; import com.mouseboy.assignment1.helpers.Operator; import com.mouseboy.assignment1.helpers.Token; +import com.mouseboy.assignment1.helpers.Utils; import java.util.ArrayList; +// yes this could've been done in a more OOP way but I miss C++ and don't want to Java :3 public class StateFarm { + // none of this state needs to be stored as it is only ever generated when public methods are called / is reset between calls private StringBuilder currentData = new StringBuilder(); private final AppCompatActivity parent; private final ArrayList tokens = new ArrayList<>(); @@ -23,79 +26,36 @@ public class StateFarm { 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 updateDisplay() { + ((TextView) parent.findViewById(R.id.output)).setText(currentData.toString()); + } + + /** + * Backspace function + */ public void clearCurrent() { + checkAndClearExceptions(); if (currentData.length() == 0) return; currentData.deleteCharAt(currentData.length() - 1); updateDisplay(); } + /** + * Clears the entire string + */ public void clearAll() { currentData = new StringBuilder(); updateDisplay(); } + /** + * Adds a number to the formula. Should be a single character + */ public void addNumber(String number) { tokenize(); // adds a multiply for you if you add a number after a paren express () @@ -105,10 +65,14 @@ public class StateFarm { updateDisplay(); } + /** + * Adds an operator to the formula. Should be a single character + */ public void addOperator(String operator) { tokenize(); + // don't add operators if there is no operands if (!tokens.isEmpty()) { - // changes operators if you haven't typed an expression + // Remove unused opening braces while (last() == Operator.ParenLeft) { currentData.deleteCharAt(currentData.length() - 1); tokens.remove(tokens.size() - 1); @@ -127,8 +91,12 @@ public class StateFarm { } } + /** + * Adds a dot to the current number in the formula. Does nothing if it is already a decimal + */ public void dot() { tokenize(); + // don't do anything if the number already has a dot in it if (!tokens.isEmpty() && last() instanceof Number) { Number v = (Number) last(); if (!v.value.contains(".")) { @@ -138,35 +106,15 @@ public class StateFarm { } } - 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, '-'); - } - } - } - + /** + * Negates the current number or parenthesis expression. removes the negative if it already exists. + */ public void neg() { tokenize(); if (tokens.isEmpty()) return; if (last() instanceof Number) { + // find beginning of number. Should store begins/end inside tokens but meh it's Java int v = currentData.length() - 1; while (v > 0 && (Character.isDigit(currentData.charAt(v)) || currentData.charAt(v) == '.')) { @@ -176,6 +124,7 @@ public class StateFarm { updateDisplay(); } if (last() == Operator.ParenRight) { + // same thing as if it is a number but we need to find the matching opening brace to the closing brace we are on int outstanding = 0; int v = currentData.length() - 1; do { @@ -185,17 +134,30 @@ public class StateFarm { 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(); } } + /** + * Evaluates the current equation, removes the formula and replaces with the result + */ public void equals() { - double value = parse(); - currentData = new StringBuilder(); - currentData.append(value); + StringBuilder newData = new StringBuilder(); + try { + double value = parse(); + newData.append(Utils.formatDecimal(value)); + } catch (Exception e) { + newData.append(e.getMessage()); + // used as a tombstone to identify if the formula entry has an error message in it + // plus: + // https://en.wikipedia.org/wiki/Tilde#Other_uses + // In modern internet slang, the tilde can be used to signify endearment or love, + // i.e. "Hello master~". It is commonly used in the furry and femboy communities + // and can also be used as a diminutive, akin to adding the "ee" sound to the end of a word.[citation needed] + newData.append("~"); + } + currentData = newData; updateDisplay(); } @@ -214,6 +176,8 @@ public class StateFarm { tokenize(); } } + // when determining what kind of parenthesis to add we need to know the current outstanding count. + // outstanding being the ones which do not have a matching closing bracket int outstanding_paren = 0; for (Token op : tokens) { if (op == Operator.ParenLeft) @@ -222,20 +186,26 @@ public class StateFarm { outstanding_paren--; } if (last() instanceof Number) { + // numbers have a special case where we will infer number(expr) means number*(expr) + // which is fairly standard math and a nice QOL feature if (outstanding_paren == 0) currentData.append("*("); else currentData.append(")"); } else { switch ((Operator) last()) { + // operators can be all the same case Plus: case Minus: case Mul: case Div: case ParenLeft: + // we just want to append a new opening brace currentData.append("("); break; case ParenRight: + // but if all paren are closed we can assume multiplying a new expression + // otherwise we should continue to close the parenthesis if (outstanding_paren == 0) currentData.append("*("); else @@ -260,63 +230,205 @@ public class StateFarm { // (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) + /** + * Starts the parser + * @return the evaluated value using precedence of the current formula + */ public double parse() { tokenize(); - return parse_pres_1(); + if (currentData.length() == 0) + throw new RuntimeException("Expression Required"); + return parsePres1(); } - private double parse_pres_1() { - double v = parse_pres_2(); + /** + * Evaluate precedence of level 1 (lowest level of precedence) + */ + private double parsePres1() { + // get the left hand side of the equation by parsing down a level in the tree. + // This will be a value an expression + double v = parsePres2(); + // if after getting the value for the lhs we find a plus or minus, apply the operator if (peek() == Operator.Plus) { next(); - v += parse_pres_2(); + v += parsePres2(); } else if (peek() == Operator.Minus) { next(); - v -= parse_pres_2(); + v -= parsePres2(); } return v; } - private double parse_pres_2() { - double v = parse_pres_3(); - if (peek() == Operator.Mul) { + /** + * Evaluate precedence of level 2 (second lowest precedence) + */ + private double parsePres2() { + // same as above, lvl 3 contains the terminals though + double v = parsePres3(); + // same as above; BEDMAS. Divide goes first. + if (peek() == Operator.Div) { next(); - v *= parse_pres_3(); - } else if (peek() == Operator.Div) { - next(); - double d = parse_pres_3(); + double d = parsePres3(); if (d == 0) - throw new ArithmeticException("Cannot divide by zero!"); + throw new ArithmeticException("Cannot divide by zero"); v /= d; + } else if (peek() == Operator.Mul) { + next(); + v *= parsePres3(); } return v; } - private double parse_pres_3() { + /** + * Evaluate precedence of level 3 (highest precedence) + */ + private double parsePres3() { + // look for either an expression, a number, or a minus sign if (peek() == Operator.ParenLeft) { next(); // consume ( - double d = parse_pres_1(); // consume expression + double d = parsePres1(); // consume expression next(); // consume ) return d; } else if (peek() == Operator.Minus) { // negating a value next(); - return -parse_pres_3(); + return -parsePres3(); } 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!"); + throw new RuntimeException("Invalid Expression"); } } - public void printAllTokens() { + /** + * Handles adding or removing the negative sign on numbers or (parenthesis groups) + * @param v one past the start of your value + * @param matching true if the two open paren are matched, otherwise false. + */ + private void handleNegativeWithSubtract(int v, boolean matching) { + // im not able to explain mess of edge cases. just accept it works like i did and move on :3 + 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, '-'); + } + } + } + + /** + * @return next token without moving the parser forward + */ + private Token peek() { + // returning null if there is none will result in all equals failing, + // which will throw an invalid expression at some point. + if (!hasNext()) + return null; + return tokens.get(currentToken); + } + + /** + * Moves the parser forward one in the token stream + */ + private void next() { + currentToken++; + } + + /** + * Returns true if there is a next value in the steam + */ + private boolean hasNext() { + return currentToken < tokens.size(); + } + + /** + * @return token at the top of the token stream. useful for input validation. + */ + private Token last() { + return tokens.get(tokens.size() - 1); + } + + /** + * Checks for an exception then clears it from the screen. + */ + private void checkAndClearExceptions() { + // this is why i used the ~. Was the simplest way. + if (currentData.toString().contains("~")) { + currentData = new StringBuilder(); + updateDisplay(); + } + } + + /** + * Function which handles all the tokenization of the current input expression string. + */ + private void tokenize() { + checkAndClearExceptions(); + 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: + // find extends of the number then add it as a token. + 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; + } + } + } + + /** + * Unused helper functions below + * ----------------------------- + */ + private void printAllTokens() { for (Token t : tokens) printToken(t); } - public void printToken(Token t) { + private void printToken(Token t) { if (t instanceof Number) System.out.println(((Number) t).value); else { @@ -343,8 +455,4 @@ public class StateFarm { } } - 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 84e9b6c..8da25c7 100644 --- a/app/src/main/res/layout-land/activity_main_calculator.xml +++ b/app/src/main/res/layout-land/activity_main_calculator.xml @@ -116,7 +116,7 @@ android:id="@+id/bc" style="@style/CalculatorButtonStyle" android:backgroundTint="@color/clearButtons" - android:text="C" /> + android:text="BS" />