/*
|
* Copyright (C) 2015 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.calculator2;
|
|
import android.content.Context;
|
import android.text.SpannableString;
|
import android.text.SpannableStringBuilder;
|
import android.text.Spanned;
|
import android.text.style.TtsSpan;
|
|
import java.io.ByteArrayOutputStream;
|
import java.io.DataInput;
|
import java.io.DataOutput;
|
import java.io.DataOutputStream;
|
import java.io.IOException;
|
import java.math.BigInteger;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.HashSet;
|
|
/**
|
* A mathematical expression represented as a sequence of "tokens".
|
* Many tokens are represented by button ids for the corresponding operator.
|
* A token may also represent the result of a previously evaluated expression.
|
* The add() method adds a token to the end of the expression. The delete method() removes one.
|
* Clear() deletes the entire expression contents. Eval() evaluates the expression,
|
* producing a UnifiedReal result.
|
* Expressions are parsed only during evaluation; no explicit parse tree is maintained.
|
*
|
* The write() method is used to save the current expression. Note that neither UnifiedReal
|
* nor the underlying CR provide a serialization facility. Thus we save all previously
|
* computed values by writing out the expression that was used to compute them, and reevaluate
|
* when reading it back in.
|
*/
|
class CalculatorExpr {
|
/**
|
* An interface for resolving expression indices in embedded subexpressions to
|
* the associated CalculatorExpr, and associating a UnifiedReal result with it.
|
* All methods are thread-safe in the strong sense; they may be called asynchronously
|
* at any time from any thread.
|
*/
|
public interface ExprResolver {
|
/*
|
* Retrieve the expression corresponding to index.
|
*/
|
CalculatorExpr getExpr(long index);
|
/*
|
* Retrieve the degree mode associated with the expression at index i.
|
*/
|
boolean getDegreeMode(long index);
|
/*
|
* Retrieve the stored result for the expression at index, or return null.
|
*/
|
UnifiedReal getResult(long index);
|
/*
|
* Atomically test for an existing result, and set it if there was none.
|
* Return the prior result if there was one, or the new one if there was not.
|
* May only be called after getExpr.
|
*/
|
UnifiedReal putResultIfAbsent(long index, UnifiedReal result);
|
}
|
|
private ArrayList<Token> mExpr; // The actual representation
|
// as a list of tokens. Constant
|
// tokens are always nonempty.
|
|
private static enum TokenKind { CONSTANT, OPERATOR, PRE_EVAL };
|
private static TokenKind[] tokenKindValues = TokenKind.values();
|
private final static BigInteger BIG_MILLION = BigInteger.valueOf(1000000);
|
private final static BigInteger BIG_BILLION = BigInteger.valueOf(1000000000);
|
|
private static abstract class Token {
|
abstract TokenKind kind();
|
|
/**
|
* Write token as either a very small Byte containing the TokenKind,
|
* followed by data needed by subclass constructor,
|
* or as a byte >= 0x20 directly describing the OPERATOR token.
|
*/
|
abstract void write(DataOutput out) throws IOException;
|
|
/**
|
* Return a textual representation of the token.
|
* The result is suitable for either display as part od the formula or TalkBack use.
|
* It may be a SpannableString that includes added TalkBack information.
|
* @param context context used for converting button ids to strings
|
*/
|
abstract CharSequence toCharSequence(Context context);
|
}
|
|
/**
|
* Representation of an operator token
|
*/
|
private static class Operator extends Token {
|
// TODO: rename id.
|
public final int id; // We use the button resource id
|
Operator(int resId) {
|
id = resId;
|
}
|
Operator(byte op) throws IOException {
|
id = KeyMaps.fromByte(op);
|
}
|
@Override
|
void write(DataOutput out) throws IOException {
|
out.writeByte(KeyMaps.toByte(id));
|
}
|
@Override
|
public CharSequence toCharSequence(Context context) {
|
String desc = KeyMaps.toDescriptiveString(context, id);
|
if (desc != null) {
|
SpannableString result = new SpannableString(KeyMaps.toString(context, id));
|
Object descSpan = new TtsSpan.TextBuilder(desc).build();
|
result.setSpan(descSpan, 0, result.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
return result;
|
} else {
|
return KeyMaps.toString(context, id);
|
}
|
}
|
@Override
|
TokenKind kind() { return TokenKind.OPERATOR; }
|
}
|
|
/**
|
* Representation of a (possibly incomplete) numerical constant.
|
* Supports addition and removal of trailing characters; hence mutable.
|
*/
|
private static class Constant extends Token implements Cloneable {
|
private boolean mSawDecimal;
|
private String mWhole; // String preceding decimal point.
|
private String mFraction; // String after decimal point.
|
private int mExponent; // Explicit exponent, only generated through addExponent.
|
private static int SAW_DECIMAL = 0x1;
|
private static int HAS_EXPONENT = 0x2;
|
|
Constant() {
|
mWhole = "";
|
mFraction = "";
|
// mSawDecimal = false;
|
// mExponent = 0;
|
};
|
|
Constant(DataInput in) throws IOException {
|
mWhole = in.readUTF();
|
byte flags = in.readByte();
|
if ((flags & SAW_DECIMAL) != 0) {
|
mSawDecimal = true;
|
mFraction = in.readUTF();
|
} else {
|
// mSawDecimal = false;
|
mFraction = "";
|
}
|
if ((flags & HAS_EXPONENT) != 0) {
|
mExponent = in.readInt();
|
}
|
}
|
|
@Override
|
void write(DataOutput out) throws IOException {
|
byte flags = (byte)((mSawDecimal ? SAW_DECIMAL : 0)
|
| (mExponent != 0 ? HAS_EXPONENT : 0));
|
out.writeByte(TokenKind.CONSTANT.ordinal());
|
out.writeUTF(mWhole);
|
out.writeByte(flags);
|
if (mSawDecimal) {
|
out.writeUTF(mFraction);
|
}
|
if (mExponent != 0) {
|
out.writeInt(mExponent);
|
}
|
}
|
|
// Given a button press, append corresponding digit.
|
// We assume id is a digit or decimal point.
|
// Just return false if this was the second (or later) decimal point
|
// in this constant.
|
// Assumes that this constant does not have an exponent.
|
public boolean add(int id) {
|
if (id == R.id.dec_point) {
|
if (mSawDecimal || mExponent != 0) return false;
|
mSawDecimal = true;
|
return true;
|
}
|
int val = KeyMaps.digVal(id);
|
if (mExponent != 0) {
|
if (Math.abs(mExponent) <= 10000) {
|
if (mExponent > 0) {
|
mExponent = 10 * mExponent + val;
|
} else {
|
mExponent = 10 * mExponent - val;
|
}
|
return true;
|
} else { // Too large; refuse
|
return false;
|
}
|
}
|
if (mSawDecimal) {
|
mFraction += val;
|
} else {
|
mWhole += val;
|
}
|
return true;
|
}
|
|
public void addExponent(int exp) {
|
// Note that adding a 0 exponent is a no-op. That's OK.
|
mExponent = exp;
|
}
|
|
/**
|
* Undo the last add or remove last exponent digit.
|
* Assumes the constant is nonempty.
|
*/
|
public void delete() {
|
if (mExponent != 0) {
|
mExponent /= 10;
|
// Once zero, it can only be added back with addExponent.
|
} else if (!mFraction.isEmpty()) {
|
mFraction = mFraction.substring(0, mFraction.length() - 1);
|
} else if (mSawDecimal) {
|
mSawDecimal = false;
|
} else {
|
mWhole = mWhole.substring(0, mWhole.length() - 1);
|
}
|
}
|
|
public boolean isEmpty() {
|
return (mSawDecimal == false && mWhole.isEmpty());
|
}
|
|
/**
|
* Produce human-readable string representation of constant, as typed.
|
* We do add digit grouping separators to the whole number, even if not typed.
|
* Result is internationalized.
|
*/
|
@Override
|
public String toString() {
|
String result;
|
if (mExponent != 0) {
|
result = mWhole;
|
} else {
|
result = StringUtils.addCommas(mWhole, 0, mWhole.length());
|
}
|
if (mSawDecimal) {
|
result += '.';
|
result += mFraction;
|
}
|
if (mExponent != 0) {
|
result += "E" + mExponent;
|
}
|
return KeyMaps.translateResult(result);
|
}
|
|
/**
|
* Return BoundedRational representation of constant, if well-formed.
|
* Result is never null.
|
*/
|
public BoundedRational toRational() throws SyntaxException {
|
String whole = mWhole;
|
if (whole.isEmpty()) {
|
if (mFraction.isEmpty()) {
|
// Decimal point without digits.
|
throw new SyntaxException();
|
} else {
|
whole = "0";
|
}
|
}
|
BigInteger num = new BigInteger(whole + mFraction);
|
BigInteger den = BigInteger.TEN.pow(mFraction.length());
|
if (mExponent > 0) {
|
num = num.multiply(BigInteger.TEN.pow(mExponent));
|
}
|
if (mExponent < 0) {
|
den = den.multiply(BigInteger.TEN.pow(-mExponent));
|
}
|
return new BoundedRational(num, den);
|
}
|
|
@Override
|
public CharSequence toCharSequence(Context context) {
|
return toString();
|
}
|
|
@Override
|
public TokenKind kind() {
|
return TokenKind.CONSTANT;
|
}
|
|
// Override clone to make it public
|
@Override
|
public Object clone() {
|
Constant result = new Constant();
|
result.mWhole = mWhole;
|
result.mFraction = mFraction;
|
result.mSawDecimal = mSawDecimal;
|
result.mExponent = mExponent;
|
return result;
|
}
|
}
|
|
/**
|
* The "token" class for previously evaluated subexpressions.
|
* We treat previously evaluated subexpressions as tokens. These are inserted when we either
|
* continue an expression after evaluating some of it, or copy an expression and paste it back
|
* in.
|
* This only contains enough information to allow us to display the expression in a
|
* formula, or reevaluate the expression with the aid of an ExprResolver; we no longer
|
* cache the result. The expression corresponding to the index can be obtained through
|
* the ExprResolver, which looks it up in a subexpression database.
|
* The representation includes a UnifiedReal value. In order to
|
* support saving and restoring, we also include the underlying expression itself, and the
|
* context (currently just degree mode) used to evaluate it. The short string representation
|
* is also stored in order to avoid potentially expensive recomputation in the UI thread.
|
*/
|
private static class PreEval extends Token {
|
public final long mIndex;
|
private final String mShortRep; // Not internationalized.
|
PreEval(long index, String shortRep) {
|
mIndex = index;
|
mShortRep = shortRep;
|
}
|
@Override
|
// This writes out only a shallow representation of the result, without
|
// information about subexpressions. To write out a deep representation, we
|
// find referenced subexpressions, and iteratively write those as well.
|
public void write(DataOutput out) throws IOException {
|
out.writeByte(TokenKind.PRE_EVAL.ordinal());
|
if (mIndex > Integer.MAX_VALUE || mIndex < Integer.MIN_VALUE) {
|
// This would be millions of expressions per day for the life of the device.
|
throw new AssertionError("Expression index too big");
|
}
|
out.writeInt((int)mIndex);
|
out.writeUTF(mShortRep);
|
}
|
PreEval(DataInput in) throws IOException {
|
mIndex = in.readInt();
|
mShortRep = in.readUTF();
|
}
|
@Override
|
public CharSequence toCharSequence(Context context) {
|
return KeyMaps.translateResult(mShortRep);
|
}
|
@Override
|
public TokenKind kind() {
|
return TokenKind.PRE_EVAL;
|
}
|
public boolean hasEllipsis() {
|
return mShortRep.lastIndexOf(KeyMaps.ELLIPSIS) != -1;
|
}
|
}
|
|
/**
|
* Read token from in.
|
*/
|
public static Token newToken(DataInput in) throws IOException {
|
byte kindByte = in.readByte();
|
if (kindByte < 0x20) {
|
TokenKind kind = tokenKindValues[kindByte];
|
switch(kind) {
|
case CONSTANT:
|
return new Constant(in);
|
case PRE_EVAL:
|
PreEval pe = new PreEval(in);
|
if (pe.mIndex == -1) {
|
// Database corrupted by earlier bug.
|
// Return a conspicuously wrong placeholder that won't lead to a crash.
|
Constant result = new Constant();
|
result.add(R.id.dec_point);
|
return result;
|
} else {
|
return pe;
|
}
|
default: throw new IOException("Bad save file format");
|
}
|
} else {
|
return new Operator(kindByte);
|
}
|
}
|
|
CalculatorExpr() {
|
mExpr = new ArrayList<Token>();
|
}
|
|
private CalculatorExpr(ArrayList<Token> expr) {
|
mExpr = expr;
|
}
|
|
/**
|
* Construct CalculatorExpr, by reading it from in.
|
*/
|
CalculatorExpr(DataInput in) throws IOException {
|
mExpr = new ArrayList<Token>();
|
int size = in.readInt();
|
for (int i = 0; i < size; ++i) {
|
mExpr.add(newToken(in));
|
}
|
}
|
|
/**
|
* Write this expression to out.
|
*/
|
public void write(DataOutput out) throws IOException {
|
int size = mExpr.size();
|
out.writeInt(size);
|
for (int i = 0; i < size; ++i) {
|
mExpr.get(i).write(out);
|
}
|
}
|
|
/**
|
* Use write() above to generate a byte array containing a serialized representation of
|
* this expression.
|
*/
|
public byte[] toBytes() {
|
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
|
try (DataOutputStream out = new DataOutputStream(byteArrayStream)) {
|
write(out);
|
} catch (IOException e) {
|
// Impossible; No IO involved.
|
throw new AssertionError("Impossible IO exception", e);
|
}
|
return byteArrayStream.toByteArray();
|
}
|
|
/**
|
* Does this expression end with a numeric constant?
|
* As opposed to an operator or preevaluated expression.
|
*/
|
boolean hasTrailingConstant() {
|
int s = mExpr.size();
|
if (s == 0) {
|
return false;
|
}
|
Token t = mExpr.get(s-1);
|
return t instanceof Constant;
|
}
|
|
/**
|
* Does this expression end with a binary operator?
|
*/
|
boolean hasTrailingBinary() {
|
int s = mExpr.size();
|
if (s == 0) return false;
|
Token t = mExpr.get(s-1);
|
if (!(t instanceof Operator)) return false;
|
Operator o = (Operator)t;
|
return (KeyMaps.isBinary(o.id));
|
}
|
|
/**
|
* Append press of button with given id to expression.
|
* If the insertion would clearly result in a syntax error, either just return false
|
* and do nothing, or make an adjustment to avoid the problem. We do the latter only
|
* for unambiguous consecutive binary operators, in which case we delete the first
|
* operator.
|
*/
|
boolean add(int id) {
|
int s = mExpr.size();
|
final int d = KeyMaps.digVal(id);
|
final boolean binary = KeyMaps.isBinary(id);
|
Token lastTok = s == 0 ? null : mExpr.get(s-1);
|
int lastOp = lastTok instanceof Operator ? ((Operator) lastTok).id : 0;
|
// Quietly replace a trailing binary operator with another one, unless the second
|
// operator is minus, in which case we just allow it as a unary minus.
|
if (binary && !KeyMaps.isPrefix(id)) {
|
if (s == 0 || lastOp == R.id.lparen || KeyMaps.isFunc(lastOp)
|
|| KeyMaps.isPrefix(lastOp) && lastOp != R.id.op_sub) {
|
return false;
|
}
|
while (hasTrailingBinary()) {
|
delete();
|
}
|
// s invalid and not used below.
|
}
|
final boolean isConstPiece = (d != KeyMaps.NOT_DIGIT || id == R.id.dec_point);
|
if (isConstPiece) {
|
// Since we treat juxtaposition as multiplication, a constant can appear anywhere.
|
if (s == 0) {
|
mExpr.add(new Constant());
|
s++;
|
} else {
|
Token last = mExpr.get(s-1);
|
if(!(last instanceof Constant)) {
|
if (last instanceof PreEval) {
|
// Add explicit multiplication to avoid confusing display.
|
mExpr.add(new Operator(R.id.op_mul));
|
s++;
|
}
|
mExpr.add(new Constant());
|
s++;
|
}
|
}
|
return ((Constant)(mExpr.get(s-1))).add(id);
|
} else {
|
mExpr.add(new Operator(id));
|
return true;
|
}
|
}
|
|
/**
|
* Add exponent to the constant at the end of the expression.
|
* Assumes there is a constant at the end of the expression.
|
*/
|
void addExponent(int exp) {
|
Token lastTok = mExpr.get(mExpr.size() - 1);
|
((Constant) lastTok).addExponent(exp);
|
}
|
|
/**
|
* Remove trailing op_add and op_sub operators.
|
*/
|
void removeTrailingAdditiveOperators() {
|
while (true) {
|
int s = mExpr.size();
|
if (s == 0) {
|
break;
|
}
|
Token lastTok = mExpr.get(s-1);
|
if (!(lastTok instanceof Operator)) {
|
break;
|
}
|
int lastOp = ((Operator) lastTok).id;
|
if (lastOp != R.id.op_add && lastOp != R.id.op_sub) {
|
break;
|
}
|
delete();
|
}
|
}
|
|
/**
|
* Append the contents of the argument expression.
|
* It is assumed that the argument expression will not change, and thus its pieces can be
|
* reused directly.
|
*/
|
public void append(CalculatorExpr expr2) {
|
int s = mExpr.size();
|
int s2 = expr2.mExpr.size();
|
// Check that we're not concatenating Constant or PreEval tokens, since the result would
|
// look like a single constant, with very mysterious results for the user.
|
if (s != 0 && s2 != 0) {
|
Token last = mExpr.get(s-1);
|
Token first = expr2.mExpr.get(0);
|
if (!(first instanceof Operator) && !(last instanceof Operator)) {
|
// Fudge it by adding an explicit multiplication. We would have interpreted it as
|
// such anyway, and this makes it recognizable to the user.
|
mExpr.add(new Operator(R.id.op_mul));
|
}
|
}
|
for (int i = 0; i < s2; ++i) {
|
mExpr.add(expr2.mExpr.get(i));
|
}
|
}
|
|
/**
|
* Undo the last key addition, if any.
|
* Or possibly remove a trailing exponent digit.
|
*/
|
public void delete() {
|
final int s = mExpr.size();
|
if (s == 0) {
|
return;
|
}
|
Token last = mExpr.get(s-1);
|
if (last instanceof Constant) {
|
Constant c = (Constant)last;
|
c.delete();
|
if (!c.isEmpty()) {
|
return;
|
}
|
}
|
mExpr.remove(s-1);
|
}
|
|
/**
|
* Remove all tokens from the expression.
|
*/
|
public void clear() {
|
mExpr.clear();
|
}
|
|
public boolean isEmpty() {
|
return mExpr.isEmpty();
|
}
|
|
/**
|
* Returns a logical deep copy of the CalculatorExpr.
|
* Operator and PreEval tokens are immutable, and thus aren't really copied.
|
*/
|
public Object clone() {
|
CalculatorExpr result = new CalculatorExpr();
|
for (Token t : mExpr) {
|
if (t instanceof Constant) {
|
result.mExpr.add((Token)(((Constant)t).clone()));
|
} else {
|
result.mExpr.add(t);
|
}
|
}
|
return result;
|
}
|
|
// Am I just a constant?
|
public boolean isConstant() {
|
if (mExpr.size() != 1) {
|
return false;
|
}
|
return mExpr.get(0) instanceof Constant;
|
}
|
|
/**
|
* Return a new expression consisting of a single token representing the current pre-evaluated
|
* expression.
|
* The caller supplies the expression index and short string representation.
|
* The expression must have been previously evaluated.
|
*/
|
public CalculatorExpr abbreviate(long index, String sr) {
|
CalculatorExpr result = new CalculatorExpr();
|
@SuppressWarnings("unchecked")
|
Token t = new PreEval(index, sr);
|
result.mExpr.add(t);
|
return result;
|
}
|
|
/**
|
* Internal evaluation functions return an EvalRet pair.
|
* We compute rational (BoundedRational) results when possible, both as a performance
|
* optimization, and to detect errors exactly when we can.
|
*/
|
private static class EvalRet {
|
public int pos; // Next position (expression index) to be parsed.
|
public final UnifiedReal val; // Constructive Real result of evaluating subexpression.
|
EvalRet(int p, UnifiedReal v) {
|
pos = p;
|
val = v;
|
}
|
}
|
|
/**
|
* Internal evaluation functions take an EvalContext argument.
|
*/
|
private static class EvalContext {
|
public final int mPrefixLength; // Length of prefix to evaluate. Not explicitly saved.
|
public final boolean mDegreeMode;
|
public final ExprResolver mExprResolver; // Reconstructed, not saved.
|
// If we add any other kinds of evaluation modes, they go here.
|
EvalContext(boolean degreeMode, int len, ExprResolver er) {
|
mDegreeMode = degreeMode;
|
mPrefixLength = len;
|
mExprResolver = er;
|
}
|
EvalContext(DataInput in, int len, ExprResolver er) throws IOException {
|
mDegreeMode = in.readBoolean();
|
mPrefixLength = len;
|
mExprResolver = er;
|
}
|
void write(DataOutput out) throws IOException {
|
out.writeBoolean(mDegreeMode);
|
}
|
}
|
|
private UnifiedReal toRadians(UnifiedReal x, EvalContext ec) {
|
if (ec.mDegreeMode) {
|
return x.multiply(UnifiedReal.RADIANS_PER_DEGREE);
|
} else {
|
return x;
|
}
|
}
|
|
private UnifiedReal fromRadians(UnifiedReal x, EvalContext ec) {
|
if (ec.mDegreeMode) {
|
return x.divide(UnifiedReal.RADIANS_PER_DEGREE);
|
} else {
|
return x;
|
}
|
}
|
|
// The following methods can all throw IndexOutOfBoundsException in the event of a syntax
|
// error. We expect that to be caught in eval below.
|
|
private boolean isOperatorUnchecked(int i, int op) {
|
Token t = mExpr.get(i);
|
if (!(t instanceof Operator)) {
|
return false;
|
}
|
return ((Operator)(t)).id == op;
|
}
|
|
private boolean isOperator(int i, int op, EvalContext ec) {
|
if (i >= ec.mPrefixLength) {
|
return false;
|
}
|
return isOperatorUnchecked(i, op);
|
}
|
|
public static class SyntaxException extends Exception {
|
public SyntaxException() {
|
super();
|
}
|
public SyntaxException(String s) {
|
super(s);
|
}
|
}
|
|
// The following functions all evaluate some kind of expression starting at position i in
|
// mExpr in a specified evaluation context. They return both the expression value (as
|
// constructive real and, if applicable, as BoundedRational) and the position of the next token
|
// that was not used as part of the evaluation.
|
// This is essentially a simple recursive descent parser combined with expression evaluation.
|
|
private EvalRet evalUnary(int i, EvalContext ec) throws SyntaxException {
|
final Token t = mExpr.get(i);
|
if (t instanceof Constant) {
|
Constant c = (Constant)t;
|
return new EvalRet(i+1,new UnifiedReal(c.toRational()));
|
}
|
if (t instanceof PreEval) {
|
final long index = ((PreEval)t).mIndex;
|
UnifiedReal res = ec.mExprResolver.getResult(index);
|
if (res == null) {
|
// We try to minimize this recursive evaluation case, but currently don't
|
// completely avoid it.
|
res = nestedEval(index, ec.mExprResolver);
|
}
|
return new EvalRet(i+1, res);
|
}
|
EvalRet argVal;
|
switch(((Operator)(t)).id) {
|
case R.id.const_pi:
|
return new EvalRet(i+1, UnifiedReal.PI);
|
case R.id.const_e:
|
return new EvalRet(i+1, UnifiedReal.E);
|
case R.id.op_sqrt:
|
// Seems to have highest precedence.
|
// Does not add implicit paren.
|
// Does seem to accept a leading minus.
|
if (isOperator(i+1, R.id.op_sub, ec)) {
|
argVal = evalUnary(i+2, ec);
|
return new EvalRet(argVal.pos, argVal.val.negate().sqrt());
|
} else {
|
argVal = evalUnary(i+1, ec);
|
return new EvalRet(argVal.pos, argVal.val.sqrt());
|
}
|
case R.id.lparen:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, argVal.val);
|
case R.id.fun_sin:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, toRadians(argVal.val, ec).sin());
|
case R.id.fun_cos:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, toRadians(argVal.val,ec).cos());
|
case R.id.fun_tan:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
UnifiedReal arg = toRadians(argVal.val, ec);
|
return new EvalRet(argVal.pos, arg.sin().divide(arg.cos()));
|
case R.id.fun_ln:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, argVal.val.ln());
|
case R.id.fun_exp:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, argVal.val.exp());
|
case R.id.fun_log:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, argVal.val.ln().divide(UnifiedReal.TEN.ln()));
|
case R.id.fun_arcsin:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, fromRadians(argVal.val.asin(), ec));
|
case R.id.fun_arccos:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, fromRadians(argVal.val.acos(), ec));
|
case R.id.fun_arctan:
|
argVal = evalExpr(i+1, ec);
|
if (isOperator(argVal.pos, R.id.rparen, ec)) {
|
argVal.pos++;
|
}
|
return new EvalRet(argVal.pos, fromRadians(argVal.val.atan(),ec));
|
default:
|
throw new SyntaxException("Unrecognized token in expression");
|
}
|
}
|
|
private static final UnifiedReal ONE_HUNDREDTH = new UnifiedReal(100).inverse();
|
|
private EvalRet evalSuffix(int i, EvalContext ec) throws SyntaxException {
|
final EvalRet tmp = evalUnary(i, ec);
|
int cpos = tmp.pos;
|
UnifiedReal val = tmp.val;
|
|
boolean isFact;
|
boolean isSquared = false;
|
while ((isFact = isOperator(cpos, R.id.op_fact, ec)) ||
|
(isSquared = isOperator(cpos, R.id.op_sqr, ec)) ||
|
isOperator(cpos, R.id.op_pct, ec)) {
|
if (isFact) {
|
val = val.fact();
|
} else if (isSquared) {
|
val = val.multiply(val);
|
} else /* percent */ {
|
val = val.multiply(ONE_HUNDREDTH);
|
}
|
++cpos;
|
}
|
return new EvalRet(cpos, val);
|
}
|
|
private EvalRet evalFactor(int i, EvalContext ec) throws SyntaxException {
|
final EvalRet result1 = evalSuffix(i, ec);
|
int cpos = result1.pos; // current position
|
UnifiedReal val = result1.val; // value so far
|
if (isOperator(cpos, R.id.op_pow, ec)) {
|
final EvalRet exp = evalSignedFactor(cpos + 1, ec);
|
cpos = exp.pos;
|
val = val.pow(exp.val);
|
}
|
return new EvalRet(cpos, val);
|
}
|
|
private EvalRet evalSignedFactor(int i, EvalContext ec) throws SyntaxException {
|
final boolean negative = isOperator(i, R.id.op_sub, ec);
|
int cpos = negative ? i + 1 : i;
|
EvalRet tmp = evalFactor(cpos, ec);
|
cpos = tmp.pos;
|
final UnifiedReal result = negative ? tmp.val.negate() : tmp.val;
|
return new EvalRet(cpos, result);
|
}
|
|
private boolean canStartFactor(int i) {
|
if (i >= mExpr.size()) return false;
|
Token t = mExpr.get(i);
|
if (!(t instanceof Operator)) return true;
|
int id = ((Operator)(t)).id;
|
if (KeyMaps.isBinary(id)) return false;
|
switch (id) {
|
case R.id.op_fact:
|
case R.id.rparen:
|
return false;
|
default:
|
return true;
|
}
|
}
|
|
private EvalRet evalTerm(int i, EvalContext ec) throws SyntaxException {
|
EvalRet tmp = evalSignedFactor(i, ec);
|
boolean is_mul = false;
|
boolean is_div = false;
|
int cpos = tmp.pos; // Current position in expression.
|
UnifiedReal val = tmp.val; // Current value.
|
while ((is_mul = isOperator(cpos, R.id.op_mul, ec))
|
|| (is_div = isOperator(cpos, R.id.op_div, ec))
|
|| canStartFactor(cpos)) {
|
if (is_mul || is_div) ++cpos;
|
tmp = evalSignedFactor(cpos, ec);
|
if (is_div) {
|
val = val.divide(tmp.val);
|
} else {
|
val = val.multiply(tmp.val);
|
}
|
cpos = tmp.pos;
|
is_mul = is_div = false;
|
}
|
return new EvalRet(cpos, val);
|
}
|
|
/**
|
* Is the subexpression starting at pos a simple percent constant?
|
* This is used to recognize exppressions like 200+10%, which we handle specially.
|
* This is defined as a Constant or PreEval token, followed by a percent sign, and followed
|
* by either nothing or an additive operator.
|
* Note that we are intentionally far more restrictive in recognizing such expressions than
|
* e.g. http://blogs.msdn.com/b/oldnewthing/archive/2008/01/10/7047497.aspx .
|
* When in doubt, we fall back to the the naive interpretation of % as 1/100.
|
* Note that 100+(10)% yields 100.1 while 100+10% yields 110. This may be controversial,
|
* but is consistent with Google web search.
|
*/
|
private boolean isPercent(int pos) {
|
if (mExpr.size() < pos + 2 || !isOperatorUnchecked(pos + 1, R.id.op_pct)) {
|
return false;
|
}
|
Token number = mExpr.get(pos);
|
if (number instanceof Operator) {
|
return false;
|
}
|
if (mExpr.size() == pos + 2) {
|
return true;
|
}
|
if (!(mExpr.get(pos + 2) instanceof Operator)) {
|
return false;
|
}
|
Operator op = (Operator) mExpr.get(pos + 2);
|
return op.id == R.id.op_add || op.id == R.id.op_sub || op.id == R.id.rparen;
|
}
|
|
/**
|
* Compute the multiplicative factor corresponding to an N% addition or subtraction.
|
* @param pos position of Constant or PreEval expression token corresponding to N.
|
* @param isSubtraction this is a subtraction, as opposed to addition.
|
* @param ec usable evaluation contex; only length matters.
|
* @return UnifiedReal value and position, which is pos + 2, i.e. after percent sign
|
*/
|
private EvalRet getPercentFactor(int pos, boolean isSubtraction, EvalContext ec)
|
throws SyntaxException {
|
EvalRet tmp = evalUnary(pos, ec);
|
UnifiedReal val = isSubtraction ? tmp.val.negate() : tmp.val;
|
val = UnifiedReal.ONE.add(val.multiply(ONE_HUNDREDTH));
|
return new EvalRet(pos + 2 /* after percent sign */, val);
|
}
|
|
private EvalRet evalExpr(int i, EvalContext ec) throws SyntaxException {
|
EvalRet tmp = evalTerm(i, ec);
|
boolean is_plus;
|
int cpos = tmp.pos;
|
UnifiedReal val = tmp.val;
|
while ((is_plus = isOperator(cpos, R.id.op_add, ec))
|
|| isOperator(cpos, R.id.op_sub, ec)) {
|
if (isPercent(cpos + 1)) {
|
tmp = getPercentFactor(cpos + 1, !is_plus, ec);
|
val = val.multiply(tmp.val);
|
} else {
|
tmp = evalTerm(cpos + 1, ec);
|
if (is_plus) {
|
val = val.add(tmp.val);
|
} else {
|
val = val.subtract(tmp.val);
|
}
|
}
|
cpos = tmp.pos;
|
}
|
return new EvalRet(cpos, val);
|
}
|
|
/**
|
* Return the starting position of the sequence of trailing binary operators.
|
*/
|
private int trailingBinaryOpsStart() {
|
int result = mExpr.size();
|
while (result > 0) {
|
Token last = mExpr.get(result - 1);
|
if (!(last instanceof Operator)) break;
|
Operator o = (Operator)last;
|
if (!KeyMaps.isBinary(o.id)) break;
|
--result;
|
}
|
return result;
|
}
|
|
/**
|
* Is the current expression worth evaluating?
|
*/
|
public boolean hasInterestingOps() {
|
final int last = trailingBinaryOpsStart();
|
int first = 0;
|
if (last > first && isOperatorUnchecked(first, R.id.op_sub)) {
|
// Leading minus is not by itself interesting.
|
first++;
|
}
|
for (int i = first; i < last; ++i) {
|
Token t1 = mExpr.get(i);
|
if (t1 instanceof Operator
|
|| t1 instanceof PreEval && ((PreEval)t1).hasEllipsis()) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
/**
|
* Does the expression contain trig operations?
|
*/
|
public boolean hasTrigFuncs() {
|
for (Token t : mExpr) {
|
if (t instanceof Operator) {
|
Operator o = (Operator)t;
|
if (KeyMaps.isTrigFunc(o.id)) {
|
return true;
|
}
|
}
|
}
|
return false;
|
}
|
|
/**
|
* Add the indices of unevaluated PreEval expressions embedded in the current expression to
|
* argument. This includes only directly referenced expressions e, not those indirectly
|
* referenced by e. If the index was already present, it is not added. If the argument
|
* contained no duplicates, the result will not either. New indices are added to the end of
|
* the list.
|
*/
|
private void addReferencedExprs(ArrayList<Long> list, ExprResolver er) {
|
for (Token t : mExpr) {
|
if (t instanceof PreEval) {
|
Long index = ((PreEval) t).mIndex;
|
if (er.getResult(index) == null && !list.contains(index)) {
|
list.add(index);
|
}
|
}
|
}
|
}
|
|
/**
|
* Return a list of unevaluated expressions transitively referenced by the current one.
|
* All expressions in the resulting list will have had er.getExpr() called on them.
|
* The resulting list is ordered such that evaluating expressions in list order
|
* should trigger few recursive evaluations.
|
*/
|
public ArrayList<Long> getTransitivelyReferencedExprs(ExprResolver er) {
|
// We could avoid triggering any recursive evaluations by actually building the
|
// dependency graph and topologically sorting it. Note that sorting by index works
|
// for positive and negative indices separately, but not their union. Currently we
|
// just settle for reverse breadth-first-search order, which handles the common case
|
// of simple dependency chains well.
|
ArrayList<Long> list = new ArrayList<Long>();
|
int scanned = 0; // We've added expressions referenced by [0, scanned) to the list
|
addReferencedExprs(list, er);
|
while (scanned != list.size()) {
|
er.getExpr(list.get(scanned++)).addReferencedExprs(list, er);
|
}
|
Collections.reverse(list);
|
return list;
|
}
|
|
/**
|
* Evaluate the expression at the given index to a UnifiedReal.
|
* Both saves and returns the result.
|
*/
|
UnifiedReal nestedEval(long index, ExprResolver er) throws SyntaxException {
|
CalculatorExpr nestedExpr = er.getExpr(index);
|
EvalContext newEc = new EvalContext(er.getDegreeMode(index),
|
nestedExpr.trailingBinaryOpsStart(), er);
|
EvalRet new_res = nestedExpr.evalExpr(0, newEc);
|
return er.putResultIfAbsent(index, new_res.val);
|
}
|
|
/**
|
* Evaluate the expression excluding trailing binary operators.
|
* Errors result in exceptions, most of which are unchecked. Should not be called
|
* concurrently with modification of the expression. May take a very long time; avoid calling
|
* from UI thread.
|
*
|
* @param degreeMode use degrees rather than radians
|
*/
|
UnifiedReal eval(boolean degreeMode, ExprResolver er) throws SyntaxException
|
// And unchecked exceptions thrown by UnifiedReal, CR,
|
// and BoundedRational.
|
{
|
// First evaluate all indirectly referenced expressions in increasing index order.
|
// This ensures that subsequent evaluation never encounters an embedded PreEval
|
// expression that has not been previously evaluated.
|
// We could do the embedded evaluations recursively, but that risks running out of
|
// stack space.
|
ArrayList<Long> referenced = getTransitivelyReferencedExprs(er);
|
for (long index : referenced) {
|
nestedEval(index, er);
|
}
|
try {
|
// We currently never include trailing binary operators, but include other trailing
|
// operators. Thus we usually, but not always, display results for prefixes of valid
|
// expressions, and don't generate an error where we previously displayed an instant
|
// result. This reflects the Android L design.
|
int prefixLen = trailingBinaryOpsStart();
|
EvalContext ec = new EvalContext(degreeMode, prefixLen, er);
|
EvalRet res = evalExpr(0, ec);
|
if (res.pos != prefixLen) {
|
throw new SyntaxException("Failed to parse full expression");
|
}
|
return res.val;
|
} catch (IndexOutOfBoundsException e) {
|
throw new SyntaxException("Unexpected expression end");
|
}
|
}
|
|
// Produce a string representation of the expression itself
|
SpannableStringBuilder toSpannableStringBuilder(Context context) {
|
SpannableStringBuilder ssb = new SpannableStringBuilder();
|
for (Token t : mExpr) {
|
ssb.append(t.toCharSequence(context));
|
}
|
return ssb;
|
}
|
}
|