main
Brett 2024-10-17 19:12:21 -04:00
parent 5b04785ab9
commit 9248d036f1
9 changed files with 243 additions and 125 deletions

BIN
ReadMe.odt Normal file

Binary file not shown.

BIN
ReadMe.pdf Normal file

Binary file not shown.

View File

@ -2,10 +2,12 @@ package com.mouseboy.assignment1;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Bundle; import android.os.Bundle;
import android.os.PersistableBundle;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import androidx.activity.EdgeToEdge; import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets; import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
@ -21,7 +23,7 @@ import java.util.ArrayList;
public class MainCalculatorActivity extends AppCompatActivity { public class MainCalculatorActivity extends AppCompatActivity {
// I missing having decltype already // I missing having decltype already
private final StateFarm state = new StateFarm(this,""); private StateFarm state = new StateFarm(this,"");
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -34,6 +36,7 @@ public class MainCalculatorActivity extends AppCompatActivity {
return insets; return insets;
}); });
// setup button listeners
findViewById(R.id.b0).setOnClickListener((View view) -> state.addNumber("0")); findViewById(R.id.b0).setOnClickListener((View view) -> state.addNumber("0"));
findViewById(R.id.b1).setOnClickListener((View view) -> state.addNumber("1")); findViewById(R.id.b1).setOnClickListener((View view) -> state.addNumber("1"));
findViewById(R.id.b2).setOnClickListener((View view) -> state.addNumber("2")); 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()); 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();
}
} }

View File

@ -1,5 +1,6 @@
package com.mouseboy.assignment1.helpers; 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 class Number implements Token {
public final String value; public final String value;

View File

@ -1,5 +1,6 @@
package com.mouseboy.assignment1.helpers; 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 { public enum Operator implements Token {
Plus, Plus,
Minus, Minus,

View File

@ -1,4 +1,5 @@
package com.mouseboy.assignment1.helpers; package com.mouseboy.assignment1.helpers;
// empty interface representing a token for the tokenizer / parser
public interface Token { public interface Token {
} }

View File

@ -7,26 +7,17 @@ import java.text.DecimalFormatSymbols;
public class Utils { public class Utils {
// why does java not have a nice decimal formatter // 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) { public static String formatDecimal(double value) {
BigDecimal decimalValue = BigDecimal.valueOf(value); BigDecimal decimalValue = BigDecimal.valueOf(value);
String pattern; String pattern;
if (decimalValue.stripTrailingZeros().scale() <= 0)
if (decimalValue.stripTrailingZeros().scale() <= 0) {
pattern = "#,##0"; pattern = "#,##0";
} else { else
pattern = "#,##0.##"; pattern = "#,##0.##";
}
DecimalFormat df = new DecimalFormat(pattern); return new DecimalFormat(pattern).format(value);
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator('.');
symbols.setGroupingSeparator(',');
df.setDecimalFormatSymbols(symbols);
return df.format(value);
} }
} }

View File

@ -8,11 +8,14 @@ import com.mouseboy.assignment1.R;
import com.mouseboy.assignment1.helpers.Number; import com.mouseboy.assignment1.helpers.Number;
import com.mouseboy.assignment1.helpers.Operator; import com.mouseboy.assignment1.helpers.Operator;
import com.mouseboy.assignment1.helpers.Token; import com.mouseboy.assignment1.helpers.Token;
import com.mouseboy.assignment1.helpers.Utils;
import java.util.ArrayList; 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 { 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 StringBuilder currentData = new StringBuilder();
private final AppCompatActivity parent; private final AppCompatActivity parent;
private final ArrayList<Token> tokens = new ArrayList<>(); private final ArrayList<Token> tokens = new ArrayList<>();
@ -23,79 +26,36 @@ public class StateFarm {
this.parent = parent; 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() { public String getString() {
return currentData.toString(); return currentData.toString();
} }
public void updateDisplay() {
((TextView) parent.findViewById(R.id.output)).setText(currentData.toString());
}
/**
* Backspace function
*/
public void clearCurrent() { public void clearCurrent() {
checkAndClearExceptions();
if (currentData.length() == 0) if (currentData.length() == 0)
return; return;
currentData.deleteCharAt(currentData.length() - 1); currentData.deleteCharAt(currentData.length() - 1);
updateDisplay(); updateDisplay();
} }
/**
* Clears the entire string
*/
public void clearAll() { public void clearAll() {
currentData = new StringBuilder(); currentData = new StringBuilder();
updateDisplay(); updateDisplay();
} }
/**
* Adds a number to the formula. Should be a single character
*/
public void addNumber(String number) { public void addNumber(String number) {
tokenize(); tokenize();
// adds a multiply for you if you add a number after a paren express () // adds a multiply for you if you add a number after a paren express ()
@ -105,10 +65,14 @@ public class StateFarm {
updateDisplay(); updateDisplay();
} }
/**
* Adds an operator to the formula. Should be a single character
*/
public void addOperator(String operator) { public void addOperator(String operator) {
tokenize(); tokenize();
// don't add operators if there is no operands
if (!tokens.isEmpty()) { if (!tokens.isEmpty()) {
// changes operators if you haven't typed an expression // Remove unused opening braces
while (last() == Operator.ParenLeft) { while (last() == Operator.ParenLeft) {
currentData.deleteCharAt(currentData.length() - 1); currentData.deleteCharAt(currentData.length() - 1);
tokens.remove(tokens.size() - 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() { public void dot() {
tokenize(); tokenize();
// don't do anything if the number already has a dot in it
if (!tokens.isEmpty() && last() instanceof Number) { if (!tokens.isEmpty() && last() instanceof Number) {
Number v = (Number) last(); Number v = (Number) last();
if (!v.value.contains(".")) { if (!v.value.contains(".")) {
@ -138,35 +106,15 @@ public class StateFarm {
} }
} }
private void handleNegativeWithSubtract(int v, boolean matching) { /**
if (currentData.charAt(v) == '-') { * Negates the current number or parenthesis expression. removes the negative if it already exists.
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() { public void neg() {
tokenize(); tokenize();
if (tokens.isEmpty()) if (tokens.isEmpty())
return; return;
if (last() instanceof Number) { if (last() instanceof Number) {
// find beginning of number. Should store begins/end inside tokens but meh it's Java
int v = currentData.length() - 1; int v = currentData.length() - 1;
while (v > 0 && while (v > 0 &&
(Character.isDigit(currentData.charAt(v)) || currentData.charAt(v) == '.')) { (Character.isDigit(currentData.charAt(v)) || currentData.charAt(v) == '.')) {
@ -176,6 +124,7 @@ public class StateFarm {
updateDisplay(); updateDisplay();
} }
if (last() == Operator.ParenRight) { 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 outstanding = 0;
int v = currentData.length() - 1; int v = currentData.length() - 1;
do { do {
@ -185,17 +134,30 @@ public class StateFarm {
outstanding--; outstanding--;
v--; v--;
} while (v > 0 && outstanding != 0); } while (v > 0 && outstanding != 0);
for (int i = v; i < currentData.length(); i++)
System.out.println(currentData.charAt(i));
handleNegativeWithSubtract(v, outstanding == 0); handleNegativeWithSubtract(v, outstanding == 0);
updateDisplay(); updateDisplay();
} }
} }
/**
* Evaluates the current equation, removes the formula and replaces with the result
*/
public void equals() { public void equals() {
StringBuilder newData = new StringBuilder();
try {
double value = parse(); double value = parse();
currentData = new StringBuilder(); newData.append(Utils.formatDecimal(value));
currentData.append(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(); updateDisplay();
} }
@ -214,6 +176,8 @@ public class StateFarm {
tokenize(); 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; int outstanding_paren = 0;
for (Token op : tokens) { for (Token op : tokens) {
if (op == Operator.ParenLeft) if (op == Operator.ParenLeft)
@ -222,20 +186,26 @@ public class StateFarm {
outstanding_paren--; outstanding_paren--;
} }
if (last() instanceof Number) { 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) if (outstanding_paren == 0)
currentData.append("*("); currentData.append("*(");
else else
currentData.append(")"); currentData.append(")");
} else { } else {
switch ((Operator) last()) { switch ((Operator) last()) {
// operators can be all the same
case Plus: case Plus:
case Minus: case Minus:
case Mul: case Mul:
case Div: case Div:
case ParenLeft: case ParenLeft:
// we just want to append a new opening brace
currentData.append("("); currentData.append("(");
break; break;
case ParenRight: 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) if (outstanding_paren == 0)
currentData.append("*("); currentData.append("*(");
else else
@ -260,63 +230,205 @@ public class StateFarm {
// (https://github.com/Tri11Paragon/blt-gp/blob/094fa76b5823f81f653ef8b4065cd15d7501cfec/include/blt/gp/program.h#L143C1-L153C22) // (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) // (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() { public double parse() {
tokenize(); 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) { if (peek() == Operator.Plus) {
next(); next();
v += parse_pres_2(); v += parsePres2();
} else if (peek() == Operator.Minus) { } else if (peek() == Operator.Minus) {
next(); next();
v -= parse_pres_2(); v -= parsePres2();
} }
return v; return v;
} }
private double parse_pres_2() { /**
double v = parse_pres_3(); * Evaluate precedence of level 2 (second lowest precedence)
if (peek() == Operator.Mul) { */
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(); next();
v *= parse_pres_3(); double d = parsePres3();
} else if (peek() == Operator.Div) {
next();
double d = parse_pres_3();
if (d == 0) if (d == 0)
throw new ArithmeticException("Cannot divide by zero!"); throw new ArithmeticException("Cannot divide by zero");
v /= d; v /= d;
} else if (peek() == Operator.Mul) {
next();
v *= parsePres3();
} }
return v; 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) { if (peek() == Operator.ParenLeft) {
next(); // consume ( next(); // consume (
double d = parse_pres_1(); // consume expression double d = parsePres1(); // consume expression
next(); // consume ) next(); // consume )
return d; return d;
} else if (peek() == Operator.Minus) { } else if (peek() == Operator.Minus) {
// negating a value // negating a value
next(); next();
return -parse_pres_3(); return -parsePres3();
} else if (peek() instanceof Number) { } else if (peek() instanceof Number) {
double value = Double.parseDouble(((Number) peek()).value); double value = Double.parseDouble(((Number) peek()).value);
next(); next();
return value; return value;
} else { } 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) for (Token t : tokens)
printToken(t); printToken(t);
} }
public void printToken(Token t) { private void printToken(Token t) {
if (t instanceof Number) if (t instanceof Number)
System.out.println(((Number) t).value); System.out.println(((Number) t).value);
else { else {
@ -343,8 +455,4 @@ public class StateFarm {
} }
} }
public void updateDisplay() {
((TextView) parent.findViewById(R.id.output)).setText(currentData.toString());
}
} }

View File

@ -116,7 +116,7 @@
android:id="@+id/bc" android:id="@+id/bc"
style="@style/CalculatorButtonStyle" style="@style/CalculatorButtonStyle"
android:backgroundTint="@color/clearButtons" android:backgroundTint="@color/clearButtons"
android:text="C" /> android:text="BS" />
</LinearLayout> </LinearLayout>