done
parent
5b04785ab9
commit
9248d036f1
Binary file not shown.
Binary file not shown.
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
package com.mouseboy.assignment1.helpers;
|
||||
|
||||
// empty interface representing a token for the tokenizer / parser
|
||||
public interface Token {
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
android:id="@+id/bc"
|
||||
style="@style/CalculatorButtonStyle"
|
||||
android:backgroundTint="@color/clearButtons"
|
||||
android:text="C" />
|
||||
android:text="BS" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
Loading…
Reference in New Issue