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.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();
}
}

View File

@ -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;

View File

@ -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,

View File

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

View File

@ -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);
}
}

View File

@ -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<Token> 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() {
StringBuilder newData = new StringBuilder();
try {
double value = parse();
currentData = new StringBuilder();
currentData.append(value);
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());
}
}

View File

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