package com.code44.finance.utils;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import com.code44.finance.R;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.LinkedList;
import java.util.Locale;
import bsh.Interpreter;
public class Calculator {
private static final String DECIMAL = ".";
private static final String PLUS = "+";
private static final String MINUS = "-";
private static final String DIVIDE = "/";
private static final String MULTIPLY = "*";
private final Context context;
private final Interpreter interpreter;
private final LinkedList<Part> parts;
private Part currentPart;
public Calculator(Context context) {
this.context = context;
this.interpreter = new Interpreter();
this.parts = new LinkedList<>();
clear();
}
public void setValue(double value) {
clear();
NumberFormat format = DecimalFormat.getInstance(Locale.ENGLISH);
format.setGroupingUsed(false);
currentPart = createPart(Type.NUMBER, format.format(value), null);
}
public void plus() {
addOperator(PLUS, context.getString(R.string.plus));
}
public void minus() {
addOperator(MINUS, context.getString(R.string.minus));
}
public void multiply() {
addOperator(MULTIPLY, context.getString(R.string.multiply));
}
public void divide() {
addOperator(DIVIDE, context.getString(R.string.divide));
}
public void decimal() {
doAction(Type.DECIMAL, DECIMAL, DECIMAL);
}
public void number(int number) {
if (number < 0 || number > 9) {
throw new IllegalArgumentException("Number must be [0, 9]");
}
doAction(Type.NUMBER, String.valueOf(number), null);
}
public void calculate() {
String result = "";
try {
result = interpreter.eval(getExpression()).toString();
final Double number = Double.parseDouble(result);
final NumberFormat format = NumberFormat.getInstance(Locale.ENGLISH);
format.setGroupingUsed(false);
result = Double.isInfinite(number) ? null : format.format(number);
} catch (Exception e) {
e.printStackTrace();
}
clear();
currentPart = new NumberPart(result);
}
public long getResult() {
return Math.round(getResultRaw() * 100);
}
public double getResultRaw() {
calculate();
double result = 0;
try {
result = Double.parseDouble(currentPart.toString());
} catch (Exception ignore) {
}
return result;
}
public boolean hasExpression() {
return parts.size() > 0;
}
public String getExpression() {
final StringBuilder sb = new StringBuilder();
for (Part part : parts) {
sb.append(part.toString());
}
sb.append(currentPart.toString());
return sb.toString();
}
public void setExpression(String expression) {
if (expression == null) {
throw new IllegalArgumentException("Expression cannot be null");
}
String exps[] = expression.split("");
for (String part : exps) {
boolean isNumber = false;
try {
int num = Integer.valueOf(part);
isNumber = true;
number(num);
} catch (NumberFormatException e) {
// Not a number
}
if (!isNumber) {
switch (part) {
case DECIMAL:
decimal();
break;
case PLUS:
plus();
break;
case MINUS:
minus();
break;
case MULTIPLY:
multiply();
break;
case DIVIDE:
divide();
break;
}
}
}
}
public CharSequence getFormattedExpression() {
final SpannableStringBuilder ssb = new SpannableStringBuilder();
for (Part part : parts) {
ssb.append(part.toFormattedString());
}
ssb.append(currentPart.toFormattedString());
return ssb;
}
public void delete() {
if (currentPart.delete()) {
// Should remove this part
if (parts.size() == 0) {
clear();
} else {
currentPart = parts.removeLast();
}
}
}
public void clear() {
parts.clear();
currentPart = createPart(Type.NUMBER, null, null);
}
private void addOperator(String value, String formattedValue) {
doAction(Type.OPERATOR, value, formattedValue);
}
private void doAction(Type type, String value, String formattedValue) {
final Part.Action action = currentPart.getAction(type, parts.size(), value);
switch (action) {
case NEW:
parts.add(currentPart);
currentPart = createPart(type, value, formattedValue);
break;
case OVERWRITE:
currentPart = createPart(type, value, formattedValue);
break;
case APPEND:
currentPart.append(value);
break;
case IGNORE:
default:
// Ignore
break;
}
}
private Part createPart(Type type, String value, String formattedString) {
switch (type) {
case NUMBER:
return new NumberPart(value);
case OPERATOR:
return new OperatorPart(value, formattedString);
case DECIMAL:
return new NumberPart("0").append(DECIMAL);
default:
throw new IllegalArgumentException("Cannot create part for type " + type);
}
}
private static enum Type {
OPERATOR, DECIMAL, NUMBER
}
private static abstract class Part {
protected final StringBuilder stringBuilder;
protected Part(String initialValue) {
this.stringBuilder = new StringBuilder();
if (!TextUtils.isEmpty(initialValue)) {
stringBuilder.append(initialValue);
}
}
@Override
public String toString() {
return stringBuilder.toString();
}
public abstract Action getAction(Type type, int partIndex, String value);
public abstract CharSequence toFormattedString();
public Part append(String value) {
if (!TextUtils.isEmpty(value)) {
stringBuilder.append(value);
}
return this;
}
public boolean delete() {
if (stringBuilder.length() == 0) {
return true;
} else {
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
return stringBuilder.length() == 0;
}
}
public int length() {
return stringBuilder.length();
}
public static enum Action {
IGNORE, APPEND, OVERWRITE, NEW
}
}
private static class OperatorPart extends Part {
final String formattedString;
private OperatorPart(String operator, String formattedString) {
super(operator);
this.formattedString = formattedString;
}
@Override
public Action getAction(Type type, int partIndex, String value) {
switch (type) {
case OPERATOR:
return Action.OVERWRITE;
case DECIMAL:
case NUMBER:
return Action.NEW;
default:
throw new IllegalArgumentException("Type " + type + " is not supported.");
}
}
@Override
public CharSequence toFormattedString() {
return formattedString;
}
}
private static class NumberPart extends Part {
private static final int MAX_DECIMALS = 10;
private NumberPart(String number) {
super(cleanupDecimals(ensureMaxDecimals(number)));
}
private static String ensureMaxDecimals(String number) {
if (TextUtils.isEmpty(number)) {
// No number
return number;
}
final int decimalPosition = number.indexOf(DECIMAL);
if (decimalPosition < 0) {
// No decimal
return number;
}
if (number.length() - decimalPosition <= MAX_DECIMALS) {
// Not too many decimals
return number;
}
return number.substring(0, decimalPosition + MAX_DECIMALS);
}
private static String cleanupDecimals(String number) {
if (TextUtils.isEmpty(number)) {
return number;
}
final int decimalPosition = number.indexOf(DECIMAL);
if (decimalPosition < 0) {
return number;
}
int index = number.length() - 1;
while (index >= decimalPosition && (number.charAt(index) == '0' || number.charAt(index) == '.')) {
number = number.substring(0, index);
index--;
}
return number;
}
@Override
public Action getAction(Type type, int partIndex, String value) {
switch (type) {
case OPERATOR:
if (partIndex == 0 && stringBuilder.length() == 0 && MINUS.equals(value)) {
return Action.APPEND;
} else if ((partIndex == 0 && stringBuilder.length() == 0) || (length() == 1 && MINUS.equals(stringBuilder.toString()))) {
return Action.IGNORE;
} else {
return Action.NEW;
}
case DECIMAL:
if (containsDecimal()) {
return Action.IGNORE;
} else {
return Action.APPEND;
}
case NUMBER:
if (length() == 0 && value.equals("0")) {
return Action.IGNORE;
} else {
return Action.APPEND;
}
default:
throw new IllegalArgumentException("Type " + type + " is not supported.");
}
}
@Override
public CharSequence toFormattedString() {
if (stringBuilder.length() == 0) {
return "0";
} else if (stringBuilder.charAt(0) == DECIMAL.charAt(0)) {
return "0" + super.toString();
} else {
return stringBuilder.toString();
}
}
@Override
public NumberPart append(String value) {
final String number = stringBuilder.toString();
final int decimalPosition = number.indexOf(DECIMAL);
if (decimalPosition < 0 || number.length() - decimalPosition <= MAX_DECIMALS) {
super.append(value);
}
return this;
}
@Override
public String toString() {
if (containsDecimal()) {
return super.toString();
} else {
return super.toString() + ".0";
}
}
private boolean containsDecimal() {
return stringBuilder.toString().contains(DECIMAL);
}
}
}