summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/calculator2/AlertDialogFragment.java54
-rw-r--r--src/com/android/calculator2/BoundedRational.java352
-rw-r--r--src/com/android/calculator2/Calculator.java56
-rw-r--r--src/com/android/calculator2/CalculatorExpr.java783
-rw-r--r--src/com/android/calculator2/CalculatorResult.java82
-rw-r--r--src/com/android/calculator2/Evaluator.java1161
6 files changed, 1442 insertions, 1046 deletions
diff --git a/src/com/android/calculator2/AlertDialogFragment.java b/src/com/android/calculator2/AlertDialogFragment.java
index bb7a50b..49f9549 100644
--- a/src/com/android/calculator2/AlertDialogFragment.java
+++ b/src/com/android/calculator2/AlertDialogFragment.java
@@ -21,22 +21,50 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
+import android.content.DialogInterface;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.widget.TextView;
-public class AlertDialogFragment extends DialogFragment {
+/**
+ * Display a message with a dismiss putton, and optionally a second button.
+ */
+public class AlertDialogFragment extends DialogFragment implements DialogInterface.OnClickListener {
+
+ public interface OnClickListener {
+ /**
+ * This method will be invoked when a button in the dialog is clicked.
+ *
+ * @param fragment the AlertDialogFragment that received the click
+ * @param which the button that was clicked (e.g.
+ * {@link DialogInterface#BUTTON_POSITIVE}) or the position
+ * of the item clicked
+ */
+ public void onClick(AlertDialogFragment fragment, int which);
+ }
private static final String NAME = AlertDialogFragment.class.getName();
private static final String KEY_MESSAGE = NAME + "_message";
private static final String KEY_BUTTON_NEGATIVE = NAME + "_button_negative";
+ private static final String KEY_BUTTON_POSITIVE = NAME + "_button_positive";
- public static void showMessageDialog(Activity activity, CharSequence message) {
+ /**
+ * Create and show a DialogFragment with the given message.
+ * @param activity originating Activity
+ * @param message displayed message
+ * @param positiveButtonLabel label for second button, if any. If non-null, activity must
+ * implement AlertDialogFragment.OnClickListener to respond.
+ */
+ public static void showMessageDialog(Activity activity, CharSequence message,
+ @Nullable CharSequence positiveButtonLabel) {
+ final AlertDialogFragment dialogFragment = new AlertDialogFragment();
final Bundle args = new Bundle();
args.putCharSequence(KEY_MESSAGE, message);
args.putCharSequence(KEY_BUTTON_NEGATIVE, activity.getString(R.string.dismiss));
-
- final AlertDialogFragment dialogFragment = new AlertDialogFragment();
+ if (positiveButtonLabel != null) {
+ args.putCharSequence(KEY_BUTTON_POSITIVE, positiveButtonLabel);
+ }
dialogFragment.setArguments(args);
dialogFragment.show(activity.getFragmentManager(), null /* tag */);
}
@@ -53,9 +81,21 @@ public class AlertDialogFragment extends DialogFragment {
final TextView textView = (TextView) inflater.inflate(R.layout.dialog_message,
null /* root */);
textView.setText(args.getCharSequence(KEY_MESSAGE));
- return new AlertDialog.Builder(context)
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setView(textView)
- .setNegativeButton(args.getCharSequence(KEY_BUTTON_NEGATIVE), null /* listener */)
- .create();
+ .setNegativeButton(args.getCharSequence(KEY_BUTTON_NEGATIVE), null /* listener */);
+ final CharSequence positiveButtonLabel = args.getCharSequence(KEY_BUTTON_POSITIVE);
+ if (positiveButtonLabel != null) {
+ builder.setPositiveButton(positiveButtonLabel, this);
+ }
+ return builder.create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Activity activity = getActivity();
+ if (activity instanceof AlertDialogFragment.OnClickListener /* always true */) {
+ ((AlertDialogFragment.OnClickListener) activity).onClick(this, which);
+ }
}
}
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
index ebaa157..dc132f6 100644
--- a/src/com/android/calculator2/BoundedRational.java
+++ b/src/com/android/calculator2/BoundedRational.java
@@ -16,22 +16,25 @@
package com.android.calculator2;
-// We implement rational numbers of bounded size.
-// If the length of the nuumerator plus the length of the denominator
-// exceeds a maximum size, we simply return null, and rely on our caller
-// do something else.
-// We currently never return null for a pure integer.
-// TODO: Reconsider that. With some care, large factorials might
-// become much faster.
-//
-// We also implement a number of irrational functions. These return
-// a non-null result only when the result is known to be rational.
import java.math.BigInteger;
import com.hp.creals.CR;
+/**
+ * Rational numbers that may turn to null if they get too big.
+ * For many operations, if the length of the nuumerator plus the length of the denominator exceeds
+ * a maximum size, we simply return null, and rely on our caller do something else.
+ * We currently never return null for a pure integer or for a BoundedRational that has just been
+ * constructed.
+ *
+ * We also implement a number of irrational functions. These return a non-null result only when
+ * the result is known to be rational.
+ */
public class BoundedRational {
+ // TODO: Consider returning null for integers. With some care, large factorials might become
+ // much faster.
// TODO: Maybe eventually make this extend Number?
+
private static final int MAX_SIZE = 800; // total, in bits
private final BigInteger mNum;
@@ -57,13 +60,19 @@ public class BoundedRational {
mDen = BigInteger.valueOf(1);
}
- // Debug or log messages only, not pretty.
+ /**
+ * Convert to String reflecting raw representation.
+ * Debug or log messages only, not pretty.
+ */
public String toString() {
return mNum.toString() + "/" + mDen.toString();
}
- // Output to user, more expensive, less useful for debugging
- // Not internationalized.
+ /**
+ * Convert to readable String.
+ * Intended for output output to user. More expensive, less useful for debugging than
+ * toString(). Not internationalized.
+ */
public String toNiceString() {
BoundedRational nicer = reduce().positiveDen();
String result = nicer.mNum.toString();
@@ -74,11 +83,16 @@ public class BoundedRational {
}
public static String toString(BoundedRational r) {
- if (r == null) return "not a small rational";
+ if (r == null) {
+ return "not a small rational";
+ }
return r.toString();
}
- // Primarily for debugging; clearly not exact
+ /**
+ * Return a double approximation.
+ * Primarily for debugging.
+ */
public double doubleValue() {
return mNum.doubleValue() / mDen.doubleValue();
}
@@ -93,39 +107,55 @@ public class BoundedRational {
}
private boolean tooBig() {
- if (mDen.equals(BigInteger.ONE)) return false;
+ if (mDen.equals(BigInteger.ONE)) {
+ return false;
+ }
return (mNum.bitLength() + mDen.bitLength() > MAX_SIZE);
}
- // return an equivalent fraction with a positive denominator.
+ /**
+ * Return an equivalent fraction with a positive denominator.
+ */
private BoundedRational positiveDen() {
- if (mDen.compareTo(BigInteger.ZERO) > 0) return this;
+ if (mDen.signum() > 0) {
+ return this;
+ }
return new BoundedRational(mNum.negate(), mDen.negate());
}
- // Return an equivalent fraction in lowest terms.
+ /**
+ * Return an equivalent fraction in lowest terms.
+ * Denominator sign may remain negative.
+ */
private BoundedRational reduce() {
- if (mDen.equals(BigInteger.ONE)) return this; // Optimization only
- BigInteger divisor = mNum.gcd(mDen);
+ if (mDen.equals(BigInteger.ONE)) {
+ return this; // Optimization only
+ }
+ final BigInteger divisor = mNum.gcd(mDen);
return new BoundedRational(mNum.divide(divisor), mDen.divide(divisor));
}
- // Return a possibly reduced version of this that's not tooBig.
- // Return null if none exists.
+ /**
+ * Return a possibly reduced version of this that's not tooBig().
+ * Return null if none exists.
+ */
private BoundedRational maybeReduce() {
- if (!tooBig()) return this;
+ if (!tooBig()) {
+ return this;
+ }
BoundedRational result = positiveDen();
- if (!result.tooBig()) return this;
result = result.reduce();
- if (!result.tooBig()) return this;
+ if (!result.tooBig()) {
+ return this;
+ }
return null;
}
public int compareTo(BoundedRational r) {
- // Compare by multiplying both sides by denominators,
- // invert result if denominator product was negative.
- return mNum.multiply(r.mDen).compareTo(r.mNum.multiply(mDen))
- * mDen.signum() * r.mDen.signum();
+ // Compare by multiplying both sides by denominators, invert result if denominator product
+ // was negative.
+ return mNum.multiply(r.mDen).compareTo(r.mNum.multiply(mDen)) * mDen.signum()
+ * r.mDen.signum();
}
public int signum() {
@@ -136,28 +166,37 @@ public class BoundedRational {
return compareTo(r) == 0;
}
- // We use static methods for arithmetic, so that we can
- // easily handle the null case.
- // We try to catch domain errors whenever possible, sometimes even when
- // one of the arguments is null, but not relevant.
+ // We use static methods for arithmetic, so that we can easily handle the null case. We try
+ // to catch domain errors whenever possible, sometimes even when one of the arguments is null,
+ // but not relevant.
- // Returns equivalent BigInteger result if it exists, null if not.
+ /**
+ * Returns equivalent BigInteger result if it exists, null if not.
+ */
public static BigInteger asBigInteger(BoundedRational r) {
- if (r == null) return null;
- if (!r.mDen.equals(BigInteger.ONE)) r = r.reduce();
- if (!r.mDen.equals(BigInteger.ONE)) return null;
- return r.mNum;
+ if (r == null) {
+ return null;
+ }
+ final BigInteger[] quotAndRem = r.mNum.divideAndRemainder(r.mDen);
+ if (quotAndRem[1].signum() == 0) {
+ return quotAndRem[0];
+ } else {
+ return null;
+ }
}
public static BoundedRational add(BoundedRational r1, BoundedRational r2) {
- if (r1 == null || r2 == null) return null;
+ if (r1 == null || r2 == null) {
+ return null;
+ }
final BigInteger den = r1.mDen.multiply(r2.mDen);
- final BigInteger num = r1.mNum.multiply(r2.mDen)
- .add(r2.mNum.multiply(r1.mDen));
+ final BigInteger num = r1.mNum.multiply(r2.mDen).add(r2.mNum.multiply(r1.mDen));
return new BoundedRational(num,den).maybeReduce();
}
public static BoundedRational negate(BoundedRational r) {
- if (r == null) return null;
+ if (r == null) {
+ return null;
+ }
return new BoundedRational(r.mNum.negate(), r.mDen);
}
@@ -166,10 +205,11 @@ public class BoundedRational {
}
static BoundedRational multiply(BoundedRational r1, BoundedRational r2) {
- // It's tempting but marginally unsound to reduce 0 * null to zero.
- // The null could represent an infinite value, for which we
- // failed to throw an exception because it was too big.
- if (r1 == null || r2 == null) return null;
+ // It's tempting but marginally unsound to reduce 0 * null to 0. The null could represent
+ // an infinite value, for which we failed to throw an exception because it was too big.
+ if (r1 == null || r2 == null) {
+ return null;
+ }
final BigInteger num = r1.mNum.multiply(r2.mNum);
final BigInteger den = r1.mDen.multiply(r2.mDen);
return new BoundedRational(num,den).maybeReduce();
@@ -181,9 +221,14 @@ public class BoundedRational {
}
}
+ /**
+ * Return the reciprocal of r (or null).
+ */
static BoundedRational inverse(BoundedRational r) {
- if (r == null) return null;
- if (r.mNum.equals(BigInteger.ZERO)) {
+ if (r == null) {
+ return null;
+ }
+ if (r.mNum.signum() == 0) {
throw new ZeroDivisionException();
}
return new BoundedRational(r.mDen, r.mNum);
@@ -194,19 +239,22 @@ public class BoundedRational {
}
static BoundedRational sqrt(BoundedRational r) {
- // Return non-null if numerator and denominator are small perfect
- // squares.
- if (r == null) return null;
+ // Return non-null if numerator and denominator are small perfect squares.
+ if (r == null) {
+ return null;
+ }
r = r.positiveDen().reduce();
- if (r.mNum.compareTo(BigInteger.ZERO) < 0) {
+ if (r.mNum.signum() < 0) {
throw new ArithmeticException("sqrt(negative)");
}
- final BigInteger num_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(
- r.mNum.doubleValue())));
- if (!num_sqrt.multiply(num_sqrt).equals(r.mNum)) return null;
- final BigInteger den_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(
- r.mDen.doubleValue())));
- if (!den_sqrt.multiply(den_sqrt).equals(r.mDen)) return null;
+ final BigInteger num_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mNum.doubleValue())));
+ if (!num_sqrt.multiply(num_sqrt).equals(r.mNum)) {
+ return null;
+ }
+ final BigInteger den_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mDen.doubleValue())));
+ if (!den_sqrt.multiply(den_sqrt).equals(r.mDen)) {
+ return null;
+ }
return new BoundedRational(num_sqrt, den_sqrt);
}
@@ -220,39 +268,45 @@ public class BoundedRational {
public final static BoundedRational THIRTY = new BoundedRational(30);
public final static BoundedRational MINUS_THIRTY = new BoundedRational(-30);
public final static BoundedRational FORTY_FIVE = new BoundedRational(45);
- public final static BoundedRational MINUS_FORTY_FIVE =
- new BoundedRational(-45);
+ public final static BoundedRational MINUS_FORTY_FIVE = new BoundedRational(-45);
public final static BoundedRational NINETY = new BoundedRational(90);
public final static BoundedRational MINUS_NINETY = new BoundedRational(-90);
private static BoundedRational map0to0(BoundedRational r) {
- if (r == null) return null;
- if (r.mNum.equals(BigInteger.ZERO)) {
+ if (r == null) {
+ return null;
+ }
+ if (r.mNum.signum() == 0) {
return ZERO;
}
return null;
}
private static BoundedRational map0to1(BoundedRational r) {
- if (r == null) return null;
- if (r.mNum.equals(BigInteger.ZERO)) {
+ if (r == null) {
+ return null;
+ }
+ if (r.mNum.signum() == 0) {
return ONE;
}
return null;
}
private static BoundedRational map1to0(BoundedRational r) {
- if (r == null) return null;
+ if (r == null) {
+ return null;
+ }
if (r.mNum.equals(r.mDen)) {
return ZERO;
}
return null;
}
- // Throw an exception if the argument is definitely out of bounds for asin
- // or acos.
+ // Throw an exception if the argument is definitely out of bounds for asin or acos.
private static void checkAsinDomain(BoundedRational r) {
- if (r == null) return;
+ if (r == null) {
+ return;
+ }
if (r.mNum.abs().compareTo(r.mDen.abs()) > 0) {
throw new ArithmeticException("inverse trig argument out of range");
}
@@ -266,9 +320,13 @@ public class BoundedRational {
public static BoundedRational degreeSin(BoundedRational r) {
final BigInteger r_BI = asBigInteger(r);
- if (r_BI == null) return null;
+ if (r_BI == null) {
+ return null;
+ }
final int r_int = r_BI.mod(BIG360).intValue();
- if (r_int % 30 != 0) return null;
+ if (r_int % 30 != 0) {
+ return null;
+ }
switch (r_int / 10) {
case 0:
return ZERO;
@@ -299,10 +357,12 @@ public class BoundedRational {
public static BoundedRational degreeAsin(BoundedRational r) {
checkAsinDomain(r);
final BigInteger r2_BI = asBigInteger(multiply(r, TWO));
- if (r2_BI == null) return null;
+ if (r2_BI == null) {
+ return null;
+ }
final int r2_int = r2_BI.intValue();
- // Somewhat surprisingly, it seems to be the case that the following
- // covers all rational cases:
+ // Somewhat surprisingly, it seems to be the case that the following covers all rational
+ // cases:
switch (r2_int) {
case -2: // Corresponding to -1 argument
return MINUS_NINETY;
@@ -320,18 +380,18 @@ public class BoundedRational {
}
public static BoundedRational tan(BoundedRational r) {
- // Unlike the degree case, we cannot check for the singularity,
- // since it occurs at an irrational argument.
+ // Unlike the degree case, we cannot check for the singularity, since it occurs at an
+ // irrational argument.
return map0to0(r);
}
public static BoundedRational degreeTan(BoundedRational r) {
- final BoundedRational degree_sin = degreeSin(r);
- final BoundedRational degree_cos = degreeCos(r);
- if (degree_cos != null && degree_cos.mNum.equals(BigInteger.ZERO)) {
+ final BoundedRational degSin = degreeSin(r);
+ final BoundedRational degCos = degreeCos(r);
+ if (degCos != null && degCos.mNum.signum() == 0) {
throw new ArithmeticException("Tangent undefined");
}
- return divide(degree_sin, degree_cos);
+ return divide(degSin, degCos);
}
public static BoundedRational atan(BoundedRational r) {
@@ -340,8 +400,12 @@ public class BoundedRational {
public static BoundedRational degreeAtan(BoundedRational r) {
final BigInteger r_BI = asBigInteger(r);
- if (r_BI == null) return null;
- if (r_BI.abs().compareTo(BigInteger.ONE) > 0) return null;
+ if (r_BI == null) {
+ return null;
+ }
+ if (r_BI.abs().compareTo(BigInteger.ONE) > 0) {
+ return null;
+ }
final int r_int = r_BI.intValue();
// Again, these seem to be all rational cases:
switch (r_int) {
@@ -376,16 +440,20 @@ public class BoundedRational {
private static final BigInteger BIG_TWO = BigInteger.valueOf(2);
- // Compute an integral power of this
+ /**
+ * Compute an integral power of this.
+ */
private BoundedRational pow(BigInteger exp) {
- if (exp.compareTo(BigInteger.ZERO) < 0) {
+ if (exp.signum() < 0) {
return inverse(pow(exp.negate()));
}
- if (exp.equals(BigInteger.ONE)) return this;
+ if (exp.equals(BigInteger.ONE)) {
+ return this;
+ }
if (exp.and(BigInteger.ONE).intValue() == 1) {
return multiply(pow(exp.subtract(BigInteger.ONE)), this);
}
- if (exp.equals(BigInteger.ZERO)) {
+ if (exp.signum() == 0) {
return ONE;
}
BoundedRational tmp = pow(exp.shiftRight(1));
@@ -396,13 +464,21 @@ public class BoundedRational {
}
public static BoundedRational pow(BoundedRational base, BoundedRational exp) {
- if (exp == null) return null;
- if (exp.mNum.equals(BigInteger.ZERO)) {
+ if (exp == null) {
+ return null;
+ }
+ if (exp.mNum.signum() == 0) {
+ // Questionable if base has undefined value. Java.lang.Math.pow() returns 1 anyway,
+ // so we do the same.
return new BoundedRational(1);
}
- if (base == null) return null;
+ if (base == null) {
+ return null;
+ }
exp = exp.reduce().positiveDen();
- if (!exp.mDen.equals(BigInteger.ONE)) return null;
+ if (!exp.mDen.equals(BigInteger.ONE)) {
+ return null;
+ }
return base.pow(exp.mNum);
}
@@ -417,12 +493,14 @@ public class BoundedRational {
return map0to1(r);
}
- // Return the base 10 log of n, if n is a power of 10, -1 otherwise.
- // n must be positive.
+ /**
+ * Return the base 10 log of n, if n is a power of 10, -1 otherwise.
+ * n must be positive.
+ */
private static long b10Log(BigInteger n) {
// This algorithm is very naive, but we doubt it matters.
long count = 0;
- while (n.mod(BigInteger.TEN).equals(BigInteger.ZERO)) {
+ while (n.mod(BigInteger.TEN).signum() == 0) {
if (Thread.interrupted()) {
throw new CR.AbortedException();
}
@@ -436,26 +514,35 @@ public class BoundedRational {
}
public static BoundedRational log(BoundedRational r) {
- if (r == null) return null;
+ if (r == null) {
+ return null;
+ }
if (r.signum() <= 0) {
throw new ArithmeticException("log(non-positive)");
}
r = r.reduce().positiveDen();
- if (r == null) return null;
+ if (r == null) {
+ return null;
+ }
if (r.mDen.equals(BigInteger.ONE)) {
long log = b10Log(r.mNum);
- if (log != -1) return new BoundedRational(log);
+ if (log != -1) {
+ return new BoundedRational(log);
+ }
} else if (r.mNum.equals(BigInteger.ONE)) {
long log = b10Log(r.mDen);
- if (log != -1) return new BoundedRational(-log);
+ if (log != -1) {
+ return new BoundedRational(-log);
+ }
}
return null;
}
- // Generalized factorial.
- // Compute n * (n - step) * (n - 2 * step) * ...
- // This can be used to compute factorial a bit faster, especially
- // if BigInteger uses sub-quadratic multiplication.
+ /**
+ * Generalized factorial.
+ * Compute n * (n - step) * (n - 2 * step) * etc. This can be used to compute factorial a bit
+ * faster, especially if BigInteger uses sub-quadratic multiplication.
+ */
private static BigInteger genFactorial(long n, long step) {
if (n > 4 * step) {
BigInteger prod1 = genFactorial(n, 2 * step);
@@ -479,61 +566,68 @@ public class BoundedRational {
}
}
- // Factorial;
- // always produces non-null (or exception) when called on non-null r.
+ /**
+ * Factorial function.
+ * Always produces non-null (or exception) when called on non-null r.
+ */
public static BoundedRational fact(BoundedRational r) {
- if (r == null) return null; // Caller should probably preclude this case.
- final BigInteger r_BI = asBigInteger(r);
- if (r_BI == null) {
+ if (r == null) {
+ return null;
+ }
+ final BigInteger rAsInt = asBigInteger(r);
+ if (rAsInt == null) {
throw new ArithmeticException("Non-integral factorial argument");
}
- if (r_BI.signum() < 0) {
+ if (rAsInt.signum() < 0) {
throw new ArithmeticException("Negative factorial argument");
}
- if (r_BI.bitLength() > 30) {
+ if (rAsInt.bitLength() > 30) {
// Will fail. LongValue() may not work. Punt now.
throw new ArithmeticException("Factorial argument too big");
}
- return new BoundedRational(genFactorial(r_BI.longValue(), 1));
+ return new BoundedRational(genFactorial(rAsInt.longValue(), 1));
}
private static final BigInteger BIG_FIVE = BigInteger.valueOf(5);
private static final BigInteger BIG_MINUS_ONE = BigInteger.valueOf(-1);
- // Return the number of decimal digits to the right of the
- // decimal point required to represent the argument exactly,
- // or Integer.MAX_VALUE if it's not possible.
- // Never returns a value les than zero, even if r is
- // a power of ten.
+ /**
+ * Return the number of decimal digits to the right of the decimal point required to represent
+ * the argument exactly.
+ * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even
+ * if r is a power of ten.
+ */
static int digitsRequired(BoundedRational r) {
- if (r == null) return Integer.MAX_VALUE;
- int powers_of_two = 0; // Max power of 2 that divides denominator
- int powers_of_five = 0; // Max power of 5 that divides denominator
+ if (r == null) {
+ return Integer.MAX_VALUE;
+ }
+ int powersOfTwo = 0; // Max power of 2 that divides denominator
+ int powersOfFive = 0; // Max power of 5 that divides denominator
// Try the easy case first to speed things up.
- if (r.mDen.equals(BigInteger.ONE)) return 0;
+ if (r.mDen.equals(BigInteger.ONE)) {
+ return 0;
+ }
r = r.reduce();
BigInteger den = r.mDen;
if (den.bitLength() > MAX_SIZE) {
return Integer.MAX_VALUE;
}
while (!den.testBit(0)) {
- ++powers_of_two;
+ ++powersOfTwo;
den = den.shiftRight(1);
}
- while (den.mod(BIG_FIVE).equals(BigInteger.ZERO)) {
- ++powers_of_five;
+ while (den.mod(BIG_FIVE).signum() == 0) {
+ ++powersOfFive;
den = den.divide(BIG_FIVE);
}
- // If the denominator has a factor of other than 2 or 5
- // (the divisors of 10), the decimal expansion does not
- // terminate. Multiplying the fraction by any number of
- // powers of 10 will not cancel the demoniator.
- // (Recall the fraction was in lowest terms to start with.)
- // Otherwise the powers of 10 we need to cancel the denominator
- // is the larger of powers_of_two and powers_of_five.
+ // If the denominator has a factor of other than 2 or 5 (the divisors of 10), the decimal
+ // expansion does not terminate. Multiplying the fraction by any number of powers of 10
+ // will not cancel the demoniator. (Recall the fraction was in lowest terms to start
+ // with.) Otherwise the powers of 10 we need to cancel the denominator is the larger of
+ // powersOfTwo and powersOfFive.
if (!den.equals(BigInteger.ONE) && !den.equals(BIG_MINUS_ONE)) {
return Integer.MAX_VALUE;
}
- return Math.max(powers_of_two, powers_of_five);
+ return Math.max(powersOfTwo, powersOfFive);
}
}
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 4eecb50..84f92c8 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -32,7 +32,9 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.app.Activity;
+import android.app.AlertDialog;
import android.content.ClipData;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
@@ -71,7 +73,8 @@ import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class Calculator extends Activity
- implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener {
+ implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
+ AlertDialogFragment.OnClickListener {
/**
* Constant for an invalid resource id.
@@ -445,19 +448,27 @@ public class Calculator extends Activity
}
}
+ /**
+ * Switch to INPUT from RESULT state in response to input of the specified button_id.
+ * View.NO_ID is treated as an incomplete function id.
+ */
+ private void switchToInput(int button_id) {
+ if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
+ mEvaluator.collapse();
+ } else {
+ announceClearedForAccessibility();
+ mEvaluator.clear();
+ }
+ setState(CalculatorState.INPUT);
+ }
+
// Add the given button id to input expression.
// If appropriate, clear the expression before doing so.
private void addKeyToExpr(int id) {
if (mCurrentState == CalculatorState.ERROR) {
setState(CalculatorState.INPUT);
} else if (mCurrentState == CalculatorState.RESULT) {
- if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
- mEvaluator.collapse();
- } else {
- announceClearForAccessibility();
- mEvaluator.clear();
- }
- setState(CalculatorState.INPUT);
+ switchToInput(id);
}
if (!mEvaluator.append(id)) {
// TODO: Some user visible feedback?
@@ -653,6 +664,11 @@ public class Calculator extends Activity
} else {
mEvaluator.delete();
}
+ if (mEvaluator.getExpr().isEmpty()
+ && (mUnprocessedChars == null || mUnprocessedChars.isEmpty())) {
+ // Resulting formula won't be announced, since it's empty.
+ announceClearedForAccessibility();
+ }
redisplayAfterFormulaChange();
}
@@ -710,8 +726,8 @@ public class Calculator extends Activity
animatorSet.start();
}
- private void announceClearForAccessibility() {
- mResultText.announceForAccessibility(getResources().getString(R.string.desc_clr));
+ private void announceClearedForAccessibility() {
+ mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
}
private void onClear() {
@@ -719,7 +735,7 @@ public class Calculator extends Activity
return;
}
cancelIfEvaluating(true);
- announceClearForAccessibility();
+ announceClearedForAccessibility();
reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -834,7 +850,15 @@ public class Calculator extends Activity
mFormulaText.setTranslationY(0.0f);
mFormulaText.requestFocus();
- }
+ }
+
+ @Override
+ public void onClick(AlertDialogFragment fragment, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ // Timeout extension request.
+ mEvaluator.setLongTimeOut();
+ }
+ }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
@@ -876,7 +900,7 @@ public class Calculator extends Activity
}
private void displayMessage(String s) {
- AlertDialogFragment.showMessageDialog(this, s);
+ AlertDialogFragment.showMessageDialog(this, s, null);
}
private void displayFraction() {
@@ -911,6 +935,10 @@ public class Calculator extends Activity
int current = 0;
int len = moreChars.length();
boolean lastWasDigit = false;
+ if (mCurrentState == CalculatorState.RESULT && len != 0) {
+ // Clear display immediately for incomplete function name.
+ switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
+ }
while (current < len) {
char c = moreChars.charAt(current);
int k = KeyMaps.keyForChar(c);
@@ -991,7 +1019,7 @@ public class Calculator extends Activity
setState(CalculatorState.INPUT);
mEvaluator.clear();
}
- mEvaluator.addSaved();
+ mEvaluator.appendSaved();
redisplayAfterFormulaChange();
} else {
addChars(item.coerceToText(this).toString(), false);
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 8a008b8..14d9236 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -36,9 +36,19 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
-// A mathematical expression represented as a sequence of "tokens".
-// Many tokes are represented by button ids for the corresponding operator.
-// Parsed only when we evaluate the expression using the "eval" method.
+/**
+ * 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 both a constructive real (CR), and possibly a BoundedRational 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 CR provides no
+ * serialization facility. Thus we save all previously computed values by writing out the
+ * expression that was used to compute them, and reevaluate on input.
+ */
class CalculatorExpr {
private ArrayList<Token> mExpr; // The actual representation
// as a list of tokens. Constant
@@ -66,41 +76,45 @@ class CalculatorExpr {
abstract CharSequence toCharSequence(Context context);
}
- // An operator token
+ /**
+ * Representation of an operator token
+ */
private static class Operator extends Token {
- final int mId; // We use the button resource id
+ public final int id; // We use the button resource id
Operator(int resId) {
- mId = resId;
+ id = resId;
}
Operator(DataInput in) throws IOException {
- mId = in.readInt();
+ id = in.readInt();
}
@Override
void write(DataOutput out) throws IOException {
out.writeByte(TokenKind.OPERATOR.ordinal());
- out.writeInt(mId);
+ out.writeInt(id);
}
@Override
public CharSequence toCharSequence(Context context) {
- String desc = KeyMaps.toDescriptiveString(context, mId);
+ String desc = KeyMaps.toDescriptiveString(context, id);
if (desc != null) {
- SpannableString result = new SpannableString(KeyMaps.toString(context, mId));
+ 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, mId);
+ return KeyMaps.toString(context, id);
}
}
@Override
TokenKind kind() { return TokenKind.OPERATOR; }
}
- // A (possibly incomplete) numerical constant.
- // Supports addition and removal of trailing characters; hence mutable.
+ /**
+ * 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;
- String mWhole; // String preceding decimal point.
+ private String mWhole; // String preceding decimal point.
private String mFraction; // String after decimal point.
private int mExponent; // Explicit exponent, only generated through addExponent.
@@ -132,7 +146,7 @@ class CalculatorExpr {
// Just return false if this was the second (or later) decimal point
// in this constant.
// Assumes that this constant does not have an exponent.
- boolean add(int id) {
+ public boolean add(int id) {
if (id == R.id.dec_point) {
if (mSawDecimal || mExponent != 0) return false;
mSawDecimal = true;
@@ -159,14 +173,16 @@ class CalculatorExpr {
return true;
}
- void addExponent(int exp) {
+ public void addExponent(int exp) {
// Note that adding a 0 exponent is a no-op. That's OK.
mExponent = exp;
}
- // Undo the last add.
- // Assumes the constant is nonempty.
- void delete() {
+ /**
+ * 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.
@@ -179,12 +195,14 @@ class CalculatorExpr {
}
}
- boolean isEmpty() {
+ public boolean isEmpty() {
return (mSawDecimal == false && mWhole.isEmpty());
}
- // Produces human-readable string, as typed.
- // Result is internationalized.
+ /**
+ * Produce human-readable string representation of constant, as typed.
+ * Result is internationalized.
+ */
@Override
public String toString() {
String result = mWhole;
@@ -198,10 +216,20 @@ class CalculatorExpr {
return KeyMaps.translateResult(result);
}
- // Return non-null BoundedRational representation.
- public BoundedRational toRational() {
+ /**
+ * Return BoundedRational representation of constant, if well-formed.
+ * Result is never null.
+ */
+ public BoundedRational toRational() throws SyntaxException {
String whole = mWhole;
- if (whole.isEmpty()) whole = "0";
+ 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) {
@@ -214,92 +242,94 @@ class CalculatorExpr {
}
@Override
- CharSequence toCharSequence(Context context) {
+ public CharSequence toCharSequence(Context context) {
return toString();
}
@Override
- TokenKind kind() { return TokenKind.CONSTANT; }
+ public TokenKind kind() {
+ return TokenKind.CONSTANT;
+ }
// Override clone to make it public
@Override
public Object clone() {
- Constant res = new Constant();
- res.mWhole = mWhole;
- res.mFraction = mFraction;
- res.mSawDecimal = mSawDecimal;
- res.mExponent = mExponent;
- return res;
+ Constant result = new Constant();
+ result.mWhole = mWhole;
+ result.mFraction = mFraction;
+ result.mSawDecimal = mSawDecimal;
+ result.mExponent = mExponent;
+ return result;
}
}
- // Hash maps used to detect duplicate subexpressions when
- // we write out CalculatorExprs and read them back in.
+ // Hash maps used to detect duplicate subexpressions when we write out CalculatorExprs and
+ // read them back in.
private static final ThreadLocal<IdentityHashMap<CR,Integer>>outMap =
- new ThreadLocal<IdentityHashMap<CR,Integer>>();
+ new ThreadLocal<IdentityHashMap<CR,Integer>>();
// Maps expressions to indices on output
private static final ThreadLocal<HashMap<Integer,PreEval>>inMap =
- new ThreadLocal<HashMap<Integer,PreEval>>();
+ new ThreadLocal<HashMap<Integer,PreEval>>();
// Maps expressions to indices on output
- private static final ThreadLocal<Integer> exprIndex =
- new ThreadLocal<Integer>();
+ private static final ThreadLocal<Integer> exprIndex = new ThreadLocal<Integer>();
- static void initExprOutput() {
+ /**
+ * Prepare for expression output.
+ * Initializes map that will lbe used to avoid duplicating shared subexpressions.
+ * This avoids a potential exponential blow-up in the expression size.
+ */
+ public static void initExprOutput() {
outMap.set(new IdentityHashMap<CR,Integer>());
exprIndex.set(Integer.valueOf(0));
}
- static void initExprInput() {
+ /**
+ * Prepare for expression input.
+ * Initializes map that will be used to reconstruct shared subexpressions.
+ */
+ public static void initExprInput() {
inMap.set(new HashMap<Integer,PreEval>());
}
- // We treat previously evaluated subexpressions as tokens
- // These are inserted when either:
- // - We continue an expression after evaluating some of it.
- // - TODO: When we copy/paste expressions.
- // The representation includes three different representations
- // of the expression:
- // 1) The CR value for use in computation.
- // 2) The integer value for use in the computations,
- // if the expression evaluates to an integer.
- // 3a) The corresponding CalculatorExpr, together with
- // 3b) The context (currently just deg/rad mode) used to evaluate
- // the expression.
- // 4) A short string representation that is used to
- // Display the expression.
- //
- // (3) is present only so that we can persist the object.
- // (4) is stored explicitly to avoid waiting for recomputation in the UI
- // thread.
+ /**
+ * 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.
+ * The representation includes both CR and possibly BoundedRational values. 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 {
- final CR mValue;
- final BoundedRational mRatValue;
+ public final CR value;
+ public final BoundedRational ratValue;
private final CalculatorExpr mExpr;
private final EvalContext mContext;
private final String mShortRep; // Not internationalized.
PreEval(CR val, BoundedRational ratVal, CalculatorExpr expr,
EvalContext ec, String shortRep) {
- mValue = val;
- mRatValue = ratVal;
+ value = val;
+ ratValue = ratVal;
mExpr = expr;
mContext = ec;
mShortRep = shortRep;
}
// In writing out PreEvals, we are careful to avoid writing
// out duplicates. We assume that two expressions are
- // duplicates if they have the same mVal. This avoids a
+ // duplicates if they have the same CR value. This avoids a
// potential exponential blow up in certain off cases and
// redundant evaluation after reading them back in.
// The parameter hash map maps expressions we've seen
// before to their index.
@Override
- void write(DataOutput out) throws IOException {
+ public void write(DataOutput out) throws IOException {
out.writeByte(TokenKind.PRE_EVAL.ordinal());
- Integer index = outMap.get().get(mValue);
+ Integer index = outMap.get().get(value);
if (index == null) {
int nextIndex = exprIndex.get() + 1;
exprIndex.set(nextIndex);
- outMap.get().put(mValue, nextIndex);
+ outMap.get().put(value, nextIndex);
out.writeInt(nextIndex);
mExpr.write(out);
mContext.write(out);
@@ -315,13 +345,10 @@ class CalculatorExpr {
if (prev == null) {
mExpr = new CalculatorExpr(in);
mContext = new EvalContext(in, mExpr.mExpr.size());
- // Recompute other fields
- // We currently do this in the UI thread, but we
- // only create PreEval expressions that were
- // previously successfully evaluated, and thus
- // don't diverge. We also only evaluate to a
- // constructive real, which involves substantial
- // work only in fairly contrived circumstances.
+ // Recompute other fields We currently do this in the UI thread, but we only
+ // create PreEval expressions that were previously successfully evaluated, and
+ // thus don't diverge. We also only evaluate to a constructive real, which
+ // involves substantial work only in fairly contrived circumstances.
// TODO: Deal better with slow evaluations.
EvalRet res = null;
try {
@@ -331,32 +358,35 @@ class CalculatorExpr {
// expressions that can be evaluated.
Log.e("Calculator", "Unexpected syntax exception" + e);
}
- mValue = res.mVal;
- mRatValue = res.mRatVal;
+ value = res.val;
+ ratValue = res.ratVal;
mShortRep = in.readUTF();
inMap.get().put(index, this);
} else {
- mValue = prev.mValue;
- mRatValue = prev.mRatValue;
+ value = prev.value;
+ ratValue = prev.ratValue;
mExpr = prev.mExpr;
mContext = prev.mContext;
mShortRep = prev.mShortRep;
}
}
@Override
- CharSequence toCharSequence(Context context) {
+ public CharSequence toCharSequence(Context context) {
return KeyMaps.translateResult(mShortRep);
}
@Override
- TokenKind kind() {
+ public TokenKind kind() {
return TokenKind.PRE_EVAL;
}
- boolean hasEllipsis() {
+ public boolean hasEllipsis() {
return mShortRep.lastIndexOf(KeyMaps.ELLIPSIS) != -1;
}
}
- static Token newToken(DataInput in) throws IOException {
+ /**
+ * Read token from in.
+ */
+ public static Token newToken(DataInput in) throws IOException {
TokenKind kind = tokenKindValues[in.readByte()];
switch(kind) {
case CONSTANT:
@@ -377,6 +407,9 @@ class CalculatorExpr {
mExpr = expr;
}
+ /**
+ * Construct CalculatorExpr, by reading it from in.
+ */
CalculatorExpr(DataInput in) throws IOException {
mExpr = new ArrayList<Token>();
int size = in.readInt();
@@ -385,7 +418,10 @@ class CalculatorExpr {
}
}
- void write(DataOutput out) throws IOException {
+ /**
+ * 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) {
@@ -393,6 +429,10 @@ class CalculatorExpr {
}
}
+ /**
+ * 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) {
@@ -402,13 +442,16 @@ class CalculatorExpr {
return t instanceof Constant;
}
+ /**
+ * Does this expression end with a binary operator?
+ */
private 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.mId));
+ return (KeyMaps.isBinary(o.id));
}
/**
@@ -420,10 +463,10 @@ class CalculatorExpr {
*/
boolean add(int id) {
int s = mExpr.size();
- int d = KeyMaps.digVal(id);
- boolean binary = KeyMaps.isBinary(id);
+ 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).mId : 0;
+ 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)) {
@@ -436,7 +479,7 @@ class CalculatorExpr {
}
// s invalid and not used below.
}
- boolean isConstPiece = (d != KeyMaps.NOT_DIGIT || id == R.id.dec_point);
+ 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) {
@@ -476,35 +519,37 @@ class CalculatorExpr {
void removeTrailingAdditiveOperators() {
while (true) {
int s = mExpr.size();
- if (s == 0) break;
+ if (s == 0) {
+ break;
+ }
Token lastTok = mExpr.get(s-1);
- if (!(lastTok instanceof Operator)) break;
- int lastOp = ((Operator) lastTok).mId;
- if (lastOp != R.id.op_add && lastOp != R.id.op_sub) break;
+ 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.
- // TODO: We probably only need this for expressions consisting of
- // a single PreEval "token", and may want to check that.
- void append(CalculatorExpr expr2) {
- // Check that we're not concatenating Constant or PreEval
- // tokens, since the result would look like a single constant
+ /**
+ * 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.
+ // 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.
+ // 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));
}
}
@@ -513,83 +558,97 @@ class CalculatorExpr {
}
}
- // Undo the last key addition, if any.
- void delete() {
- int s = mExpr.size();
- if (s == 0) return;
+ /**
+ * 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;
+ if (!c.isEmpty()) {
+ return;
+ }
}
mExpr.remove(s-1);
}
- void clear() {
+ /**
+ * Remove all tokens from the expression.
+ */
+ public void clear() {
mExpr.clear();
}
- boolean isEmpty() {
+ 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.
+ /**
+ * Returns a logical deep copy of the CalculatorExpr.
+ * Operator and PreEval tokens are immutable, and thus aren't really copied.
+ */
public Object clone() {
- CalculatorExpr res = new CalculatorExpr();
+ CalculatorExpr result = new CalculatorExpr();
for (Token t: mExpr) {
if (t instanceof Constant) {
- res.mExpr.add((Token)(((Constant)t).clone()));
+ result.mExpr.add((Token)(((Constant)t).clone()));
} else {
- res.mExpr.add(t);
+ result.mExpr.add(t);
}
}
- return res;
+ return result;
}
// Am I just a constant?
- boolean isConstant() {
- if (mExpr.size() != 1) return false;
+ public boolean isConstant() {
+ if (mExpr.size() != 1) {
+ return false;
+ }
return mExpr.get(0) instanceof Constant;
}
- // Return a new expression consisting of a single PreEval token
- // representing the current expression.
- // The caller supplies the value, degree mode, and short
- // string representation, which must have been previously computed.
- // Thus this is guaranteed to terminate reasonably quickly.
- CalculatorExpr abbreviate(CR val, BoundedRational ratVal,
+ /**
+ * Return a new expression consisting of a single token representing the current pre-evaluated
+ * expression.
+ * The caller supplies the value, degree mode, and short string representation, which must
+ * have been previously computed. Thus this is guaranteed to terminate reasonably quickly.
+ */
+ public CalculatorExpr abbreviate(CR val, BoundedRational ratVal,
boolean dm, String sr) {
CalculatorExpr result = new CalculatorExpr();
- Token t = new PreEval(val, ratVal,
- new CalculatorExpr(
- (ArrayList<Token>)mExpr.clone()),
- new EvalContext(dm, mExpr.size()), sr);
+ Token t = new PreEval(val, ratVal, new CalculatorExpr((ArrayList<Token>) mExpr.clone()),
+ new EvalContext(dm, mExpr.size()), sr);
result.mExpr.add(t);
return result;
}
- // Internal evaluation functions return an EvalRet triple.
- // We compute rational (BoundedRational) results when possible, both as
- // a performance optimization, and to detect errors exactly when we can.
- private class EvalRet {
- int mPos; // Next position (expression index) to be parsed
- final CR mVal; // Constructive Real result of evaluating subexpression
- final BoundedRational mRatVal; // Exact Rational value or null if
- // irrational or hard to compute.
+ /**
+ * Internal evaluation functions return an EvalRet triple.
+ * 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 CR val; // Constructive Real result of evaluating subexpression.
+ public final BoundedRational ratVal; // Exact Rational value or null.
EvalRet(int p, CR v, BoundedRational r) {
- mPos = p;
- mVal = v;
- mRatVal = r;
+ pos = p;
+ val = v;
+ ratVal = r;
}
}
- // And take a context argument:
+ /**
+ * 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 int mPrefixLength; // Length of prefix to evaluate. Not explicitly saved.
public final boolean mDegreeMode;
// If we add any other kinds of evaluation modes, they go here.
EvalContext(boolean degreeMode, int len) {
@@ -625,22 +684,25 @@ class CalculatorExpr {
}
}
- // The following methods can all throw IndexOutOfBoundsException
- // in the event of a syntax error. We expect that to be caught in
- // eval below.
+ // 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)).mId == op;
+ 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;
+ if (i >= ec.mPrefixLength) {
+ return false;
+ }
return isOperatorUnchecked(i, op);
}
- static class SyntaxException extends Exception {
+ public static class SyntaxException extends Exception {
public SyntaxException() {
super();
}
@@ -649,27 +711,26 @@ class CalculatorExpr {
}
}
- // 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 BigInteger) and the position of the next token
+ // 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 {
- Token t = mExpr.get(i);
+ final Token t = mExpr.get(i);
BoundedRational ratVal;
- CR value;
if (t instanceof Constant) {
Constant c = (Constant)t;
ratVal = c.toRational();
- value = ratVal.CRValue();
- return new EvalRet(i+1, value, ratVal);
+ return new EvalRet(i+1, ratVal.CRValue(), ratVal);
}
if (t instanceof PreEval) {
- PreEval p = (PreEval)t;
- return new EvalRet(i+1, p.mValue, p.mRatValue);
+ final PreEval p = (PreEval)t;
+ return new EvalRet(i+1, p.value, p.ratValue);
}
EvalRet argVal;
- switch(((Operator)(t)).mId) {
+ switch(((Operator)(t)).id) {
case R.id.const_pi:
return new EvalRet(i+1, CR.PI, null);
case R.id.const_e:
@@ -680,111 +741,144 @@ class CalculatorExpr {
// Does seem to accept a leading minus.
if (isOperator(i+1, R.id.op_sub, ec)) {
argVal = evalUnary(i+2, ec);
- ratVal = BoundedRational.sqrt(
- BoundedRational.negate(argVal.mRatVal));
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- argVal.mVal.negate().sqrt(), null);
+ ratVal = BoundedRational.sqrt(BoundedRational.negate(argVal.ratVal));
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos,
+ argVal.val.negate().sqrt(), null);
} else {
argVal = evalUnary(i+1, ec);
- ratVal = BoundedRational.sqrt(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos, argVal.mVal.sqrt(), null);
+ ratVal = BoundedRational.sqrt(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos, argVal.val.sqrt(), null);
}
case R.id.lparen:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- return new EvalRet(argVal.mPos, argVal.mVal, argVal.mRatVal);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ return new EvalRet(argVal.pos, argVal.val, argVal.ratVal);
case R.id.fun_sin:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = ec.mDegreeMode ? BoundedRational.degreeSin(argVal.mRatVal)
- : BoundedRational.sin(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- toRadians(argVal.mVal,ec).sin(), null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = ec.mDegreeMode ? BoundedRational.degreeSin(argVal.ratVal)
+ : BoundedRational.sin(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos, toRadians(argVal.val,ec).sin(), null);
case R.id.fun_cos:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = ec.mDegreeMode ? BoundedRational.degreeCos(argVal.mRatVal)
- : BoundedRational.cos(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- toRadians(argVal.mVal,ec).cos(), null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = ec.mDegreeMode ? BoundedRational.degreeCos(argVal.ratVal)
+ : BoundedRational.cos(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos, toRadians(argVal.val,ec).cos(), null);
case R.id.fun_tan:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = ec.mDegreeMode ? BoundedRational.degreeTan(argVal.mRatVal)
- : BoundedRational.tan(argVal.mRatVal);
- if (ratVal != null) break;
- CR argCR = toRadians(argVal.mVal, ec);
- return new EvalRet(argVal.mPos,
- argCR.sin().divide(argCR.cos()), null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = ec.mDegreeMode ? BoundedRational.degreeTan(argVal.ratVal)
+ : BoundedRational.tan(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ CR argCR = toRadians(argVal.val, ec);
+ return new EvalRet(argVal.pos, argCR.sin().divide(argCR.cos()), null);
case R.id.fun_ln:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = BoundedRational.ln(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos, argVal.mVal.ln(), null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = BoundedRational.ln(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos, argVal.val.ln(), null);
case R.id.fun_exp:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = BoundedRational.exp(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos, argVal.mVal.exp(), null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = BoundedRational.exp(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos, argVal.val.exp(), null);
case R.id.fun_log:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = BoundedRational.log(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- argVal.mVal.ln().divide(CR.valueOf(10).ln()),
- null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = BoundedRational.log(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos, argVal.val.ln().divide(CR.valueOf(10).ln()), null);
case R.id.fun_arcsin:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = ec.mDegreeMode ? BoundedRational.degreeAsin(argVal.mRatVal)
- : BoundedRational.asin(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- fromRadians(UnaryCRFunction
- .asinFunction.execute(argVal.mVal),ec),
- null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = ec.mDegreeMode ? BoundedRational.degreeAsin(argVal.ratVal)
+ : BoundedRational.asin(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos,
+ fromRadians(UnaryCRFunction.asinFunction.execute(argVal.val),ec), null);
case R.id.fun_arccos:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = ec.mDegreeMode ? BoundedRational.degreeAcos(argVal.mRatVal)
- : BoundedRational.acos(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- fromRadians(UnaryCRFunction
- .acosFunction.execute(argVal.mVal),ec),
- null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = ec.mDegreeMode ? BoundedRational.degreeAcos(argVal.ratVal)
+ : BoundedRational.acos(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos,
+ fromRadians(UnaryCRFunction.acosFunction.execute(argVal.val),ec), null);
case R.id.fun_arctan:
argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
- ratVal = ec.mDegreeMode ? BoundedRational.degreeAtan(argVal.mRatVal)
- : BoundedRational.atan(argVal.mRatVal);
- if (ratVal != null) break;
- return new EvalRet(argVal.mPos,
- fromRadians(UnaryCRFunction
- .atanFunction.execute(argVal.mVal),ec),
- null);
+ if (isOperator(argVal.pos, R.id.rparen, ec)) {
+ argVal.pos++;
+ }
+ ratVal = ec.mDegreeMode ? BoundedRational.degreeAtan(argVal.ratVal)
+ : BoundedRational.atan(argVal.ratVal);
+ if (ratVal != null) {
+ break;
+ }
+ return new EvalRet(argVal.pos,
+ fromRadians(UnaryCRFunction.atanFunction.execute(argVal.val),ec), null);
default:
throw new SyntaxException("Unrecognized token in expression");
}
// We have a rational value.
- return new EvalRet(argVal.mPos, ratVal.CRValue(), ratVal);
+ return new EvalRet(argVal.pos, ratVal.CRValue(), ratVal);
}
- // Compute an integral power of a constructive real.
- // Unlike the "general" case using logarithms, this handles a negative
- // base.
+ /**
+ * Compute an integral power of a constructive real.
+ * Unlike the "general" case using logarithms, this handles a negative base.
+ */
private static CR pow(CR base, BigInteger exp) {
if (exp.compareTo(BigInteger.ZERO) < 0) {
return pow(base, exp.negate()).inverse();
}
- if (exp.equals(BigInteger.ONE)) return base;
+ if (exp.equals(BigInteger.ONE)) {
+ return base;
+ }
if (exp.and(BigInteger.ONE).intValue() == 1) {
return pow(base, exp.subtract(BigInteger.ONE)).multiply(base);
}
@@ -795,24 +889,23 @@ class CalculatorExpr {
return tmp.multiply(tmp);
}
+ // Number of bits past binary point to test for integer-ness.
private static final int TEST_PREC = -100;
- // Test for integer-ness to 100 bits past binary point.
private static final BigInteger MASK =
BigInteger.ONE.shiftLeft(-TEST_PREC).subtract(BigInteger.ONE);
private static final CR REAL_E = CR.valueOf(1).exp();
private static final CR REAL_ONE_HUNDREDTH = CR.valueOf(100).inverse();
- private static final BoundedRational RATIONAL_ONE_HUNDREDTH =
- new BoundedRational(1,100);
+ private static final BoundedRational RATIONAL_ONE_HUNDREDTH = new BoundedRational(1,100);
private static boolean isApprInt(CR x) {
BigInteger appr = x.get_appr(TEST_PREC);
return appr.and(MASK).signum() == 0;
}
private EvalRet evalSuffix(int i, EvalContext ec) throws SyntaxException {
- EvalRet tmp = evalUnary(i, ec);
- int cpos = tmp.mPos;
- CR cval = tmp.mVal;
- BoundedRational ratVal = tmp.mRatVal;
+ final EvalRet tmp = evalUnary(i, ec);
+ int cpos = tmp.pos;
+ CR crVal = tmp.val;
+ BoundedRational ratVal = tmp.ratVal;
boolean isFact;
boolean isSquared = false;
while ((isFact = isOperator(cpos, R.id.op_fact, ec)) ||
@@ -820,46 +913,45 @@ class CalculatorExpr {
isOperator(cpos, R.id.op_pct, ec)) {
if (isFact) {
if (ratVal == null) {
- // Assume it was an integer, but we
- // didn't figure it out.
+ // Assume it was an integer, but we didn't figure it out.
// KitKat may have used the Gamma function.
- if (!isApprInt(cval)) {
+ if (!isApprInt(crVal)) {
throw new ArithmeticException("factorial(non-integer)");
}
- ratVal = new BoundedRational(cval.BigIntegerValue());
+ ratVal = new BoundedRational(crVal.BigIntegerValue());
}
ratVal = BoundedRational.fact(ratVal);
- cval = ratVal.CRValue();
+ crVal = ratVal.CRValue();
} else if (isSquared) {
ratVal = BoundedRational.multiply(ratVal, ratVal);
if (ratVal == null) {
- cval = cval.multiply(cval);
+ crVal = crVal.multiply(crVal);
} else {
- cval = ratVal.CRValue();
+ crVal = ratVal.CRValue();
}
} else /* percent */ {
ratVal = BoundedRational.multiply(ratVal, RATIONAL_ONE_HUNDREDTH);
if (ratVal == null) {
- cval = cval.multiply(REAL_ONE_HUNDREDTH);
+ crVal = crVal.multiply(REAL_ONE_HUNDREDTH);
} else {
- cval = ratVal.CRValue();
+ crVal = ratVal.CRValue();
}
}
++cpos;
}
- return new EvalRet(cpos, cval, ratVal);
+ return new EvalRet(cpos, crVal, ratVal);
}
private EvalRet evalFactor(int i, EvalContext ec) throws SyntaxException {
final EvalRet result1 = evalSuffix(i, ec);
- int cpos = result1.mPos; // current position
- CR cval = result1.mVal; // value so far
- BoundedRational ratVal = result1.mRatVal; // int value so far
+ int cpos = result1.pos; // current position
+ CR crVal = result1.val; // value so far
+ BoundedRational ratVal = result1.ratVal; // int value so far
if (isOperator(cpos, R.id.op_pow, ec)) {
- final EvalRet exp = evalSignedFactor(cpos+1, ec);
- cpos = exp.mPos;
+ final EvalRet exp = evalSignedFactor(cpos + 1, ec);
+ cpos = exp.pos;
// Try completely rational evaluation first.
- ratVal = BoundedRational.pow(ratVal, exp.mRatVal);
+ ratVal = BoundedRational.pow(ratVal, exp.ratVal);
if (ratVal != null) {
return new EvalRet(cpos, ratVal.CRValue(), ratVal);
}
@@ -867,33 +959,33 @@ class CalculatorExpr {
// Thus we handle that case separately.
// We punt if the exponent is an integer computed from irrational
// values. That wouldn't work reliably with floating point either.
- BigInteger int_exp = BoundedRational.asBigInteger(exp.mRatVal);
+ BigInteger int_exp = BoundedRational.asBigInteger(exp.ratVal);
if (int_exp != null) {
- cval = pow(cval, int_exp);
+ crVal = pow(crVal, int_exp);
} else {
- cval = cval.ln().multiply(exp.mVal).exp();
+ crVal = crVal.ln().multiply(exp.val).exp();
}
ratVal = null;
}
- return new EvalRet(cpos, cval, ratVal);
+ return new EvalRet(cpos, crVal, ratVal);
}
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.mPos;
- CR cval = negative ? tmp.mVal.negate() : tmp.mVal;
- BoundedRational ratVal = negative ? BoundedRational.negate(tmp.mRatVal)
- : tmp.mRatVal;
- return new EvalRet(cpos, cval, ratVal);
+ cpos = tmp.pos;
+ CR crVal = negative ? tmp.val.negate() : tmp.val;
+ BoundedRational ratVal = negative ? BoundedRational.negate(tmp.ratVal)
+ : tmp.ratVal;
+ return new EvalRet(cpos, crVal, ratVal);
}
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)).mId;
+ int id = ((Operator)(t)).id;
if (KeyMaps.isBinary(id)) return false;
switch (id) {
case R.id.op_fact:
@@ -908,72 +1000,136 @@ class CalculatorExpr {
EvalRet tmp = evalSignedFactor(i, ec);
boolean is_mul = false;
boolean is_div = false;
- int cpos = tmp.mPos; // Current position in expression.
- CR cval = tmp.mVal; // Current value.
- BoundedRational ratVal = tmp.mRatVal; // Current rational value.
+ int cpos = tmp.pos; // Current position in expression.
+ CR crVal = tmp.val; // Current value.
+ BoundedRational ratVal = tmp.ratVal; // Current rational 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) {
- ratVal = BoundedRational.divide(ratVal, tmp.mRatVal);
+ ratVal = BoundedRational.divide(ratVal, tmp.ratVal);
if (ratVal == null) {
- cval = cval.divide(tmp.mVal);
+ crVal = crVal.divide(tmp.val);
} else {
- cval = ratVal.CRValue();
+ crVal = ratVal.CRValue();
}
} else {
- ratVal = BoundedRational.multiply(ratVal, tmp.mRatVal);
+ ratVal = BoundedRational.multiply(ratVal, tmp.ratVal);
if (ratVal == null) {
- cval = cval.multiply(tmp.mVal);
+ crVal = crVal.multiply(tmp.val);
} else {
- cval = ratVal.CRValue();
+ crVal = ratVal.CRValue();
}
}
- cpos = tmp.mPos;
+ cpos = tmp.pos;
is_mul = is_div = false;
}
- return new EvalRet(cpos, cval, ratVal);
+ return new EvalRet(cpos, crVal, ratVal);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 Rational and CR values; position is pos + 2, i.e. after percent sign
+ */
+ private EvalRet getPercentFactor(int pos, boolean isSubtraction, EvalContext ec)
+ throws SyntaxException {
+ EvalRet tmp = evalUnary(pos, ec);
+ BoundedRational ratVal = isSubtraction ? BoundedRational.negate(tmp.ratVal)
+ : tmp.ratVal;
+ CR crVal = isSubtraction ? tmp.val.negate() : tmp.val;
+ ratVal = BoundedRational.add(BoundedRational.ONE,
+ BoundedRational.multiply(ratVal, RATIONAL_ONE_HUNDREDTH));
+ if (ratVal == null) {
+ crVal = CR.ONE.add(crVal.multiply(REAL_ONE_HUNDREDTH));
+ } else {
+ crVal = ratVal.CRValue();
+ }
+ return new EvalRet(pos + 2 /* after percent sign */, crVal, ratVal);
}
private EvalRet evalExpr(int i, EvalContext ec) throws SyntaxException {
EvalRet tmp = evalTerm(i, ec);
boolean is_plus;
- int cpos = tmp.mPos;
- CR cval = tmp.mVal;
- BoundedRational ratVal = tmp.mRatVal;
+ int cpos = tmp.pos;
+ CR crVal = tmp.val;
+ BoundedRational ratVal = tmp.ratVal;
while ((is_plus = isOperator(cpos, R.id.op_add, ec))
|| isOperator(cpos, R.id.op_sub, ec)) {
- tmp = evalTerm(cpos+1, ec);
- if (is_plus) {
- ratVal = BoundedRational.add(ratVal, tmp.mRatVal);
+ if (isPercent(cpos + 1)) {
+ tmp = getPercentFactor(cpos + 1, !is_plus, ec);
+ ratVal = BoundedRational.multiply(ratVal, tmp.ratVal);
if (ratVal == null) {
- cval = cval.add(tmp.mVal);
+ crVal = crVal.multiply(tmp.val);
} else {
- cval = ratVal.CRValue();
+ crVal = ratVal.CRValue();
}
} else {
- ratVal = BoundedRational.subtract(ratVal, tmp.mRatVal);
- if (ratVal == null) {
- cval = cval.subtract(tmp.mVal);
+ tmp = evalTerm(cpos + 1, ec);
+ if (is_plus) {
+ ratVal = BoundedRational.add(ratVal, tmp.ratVal);
+ if (ratVal == null) {
+ crVal = crVal.add(tmp.val);
+ } else {
+ crVal = ratVal.CRValue();
+ }
} else {
- cval = ratVal.CRValue();
+ ratVal = BoundedRational.subtract(ratVal, tmp.ratVal);
+ if (ratVal == null) {
+ crVal = crVal.subtract(tmp.val);
+ } else {
+ crVal = ratVal.CRValue();
+ }
}
}
- cpos = tmp.mPos;
+ cpos = tmp.pos;
}
- return new EvalRet(cpos, cval, ratVal);
+ return new EvalRet(cpos, crVal, ratVal);
}
- // Externally visible evaluation result.
- public class EvalResult {
- EvalResult (CR val, BoundedRational ratVal) {
- mVal = val;
- mRatVal = ratVal;
+ /**
+ * Externally visible evaluation result.
+ */
+ public static class EvalResult {
+ public final CR val;
+ public final BoundedRational ratVal;
+ EvalResult (CR v, BoundedRational rv) {
+ val = v;
+ ratVal = rv;
}
- final CR mVal;
- final BoundedRational mRatVal;
}
/**
@@ -985,13 +1141,15 @@ class CalculatorExpr {
Token last = mExpr.get(result - 1);
if (!(last instanceof Operator)) break;
Operator o = (Operator)last;
- if (!KeyMaps.isBinary(o.mId)) break;
+ if (!KeyMaps.isBinary(o.id)) break;
--result;
}
return result;
}
- // Is the current expression worth evaluating?
+ /**
+ * Is the current expression worth evaluating?
+ */
public boolean hasInterestingOps() {
int last = trailingBinaryOpsStart();
int first = 0;
@@ -1011,9 +1169,9 @@ class CalculatorExpr {
/**
* 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.
+ * 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
*/
@@ -1022,18 +1180,17 @@ class CalculatorExpr {
// and BoundedRational.
{
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.
+ // 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);
EvalRet res = evalExpr(0, ec);
- if (res.mPos != prefixLen) {
+ if (res.pos != prefixLen) {
throw new SyntaxException("Failed to parse full expression");
}
- return new EvalResult(res.mVal, res.mRatVal);
+ return new EvalResult(res.val, res.ratVal);
} catch (IndexOutOfBoundsException e) {
throw new SyntaxException("Unexpected expression end");
}
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index 1efa67b..5c96867 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -335,9 +335,9 @@ public class CalculatorResult extends AlignedTextView {
/*
* Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
- * Unlike Evaluator.getMsdPos, we treat a final 1 as significant.
+ * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
*/
- public static int getNaiveMsdIndex(String s) {
+ public static int getNaiveMsdIndexOf(String s) {
int len = s.length();
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
@@ -349,14 +349,14 @@ public class CalculatorResult extends AlignedTextView {
}
// Format a result returned by Evaluator.getString() into a single line containing ellipses
- // (if appropriate) and an exponent (if appropriate). prec is the value that was passed to
- // getString and thus identifies the significance of the rightmost digit.
+ // (if appropriate) and an exponent (if appropriate). precOffset is the value that was passed
+ // to getString and thus identifies the significance of the rightmost digit.
// A value of 1 means the rightmost digits corresponds to tenths.
// maxDigs is the maximum number of characters in the result.
// We set lastDisplayedOffset[0] to the offset of the last digit actually appearing in
// the display.
// If forcePrecision is true, we make sure that the last displayed digit corresponds to
- // prec, and allow maxDigs to be exceeded in assing the exponent.
+ // precOffset, and allow maxDigs to be exceeded in assing the exponent.
// We add two distinct kinds of exponents:
// (1) If the final result contains the leading digit we use standard scientific notation.
// (2) If not, we add an exponent corresponding to an interpretation of the final result as
@@ -369,9 +369,13 @@ public class CalculatorResult extends AlignedTextView {
public String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
boolean negative, int lastDisplayedOffset[], boolean forcePrecision) {
final int minusSpace = negative ? 1 : 0;
- final int msdIndex = truncated ? -1 : getNaiveMsdIndex(in); // INVALID_MSD is OK.
- final int decIndex = in.indexOf('.');
+ final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
String result = in;
+ if (truncated || (negative && result.charAt(0) != '-')) {
+ result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
+ // Ellipsis may be removed again in the type(1) scientific notation case.
+ }
+ final int decIndex = result.indexOf('.');
lastDisplayedOffset[0] = precOffset;
if ((decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
&& msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
@@ -400,42 +404,38 @@ public class CalculatorResult extends AlignedTextView {
exponent = initExponent + resLen - msdIndex - 1;
hasPoint = true;
}
- if (exponent != 0 || truncated) {
- // Actually add the exponent of either type:
- if (!forcePrecision) {
- int dropDigits; // Digits to drop to make room for exponent.
- if (hasPoint) {
- // Type (1) exponent.
- // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
- dropDigits = expLen(exponent);
- if (dropDigits >= result.length() - 1) {
- // Jumpy is better than no mantissa. Probably impossible anyway.
- dropDigits = Math.max(result.length() - 2, 0);
- }
- } else {
- // Type (2) exponent.
- // Exponent depends on the number of digits we drop, which depends on
- // exponent ...
- for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
- ++dropDigits) {}
- exponent = initExponent + dropDigits;
- if (precOffset - dropDigits > mLsdOffset) {
- // This can happen if e.g. result = 10^40 + 10^10
- // It turns out we would otherwise display ...10e9 because it takes
- // the same amount of space as ...1e10 but shows one more digit.
- // But we don't want to display a trailing zero, even if it's free.
- ++dropDigits;
- ++exponent;
- }
+ // Exponent can't be zero.
+ // Actually add the exponent of either type:
+ if (!forcePrecision) {
+ int dropDigits; // Digits to drop to make room for exponent.
+ if (hasPoint) {
+ // Type (1) exponent.
+ // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
+ dropDigits = expLen(exponent);
+ if (dropDigits >= result.length() - 1) {
+ // Jumpy is better than no mantissa. Probably impossible anyway.
+ dropDigits = Math.max(result.length() - 2, 0);
+ }
+ } else {
+ // Type (2) exponent.
+ // Exponent depends on the number of digits we drop, which depends on
+ // exponent ...
+ for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
+ ++dropDigits) {}
+ exponent = initExponent + dropDigits;
+ if (precOffset - dropDigits > mLsdOffset) {
+ // This can happen if e.g. result = 10^40 + 10^10
+ // It turns out we would otherwise display ...10e9 because it takes
+ // the same amount of space as ...1e10 but shows one more digit.
+ // But we don't want to display a trailing zero, even if it's free.
+ ++dropDigits;
+ ++exponent;
}
- result = result.substring(0, result.length() - dropDigits);
- lastDisplayedOffset[0] -= dropDigits;
}
- result = result + "E" + Integer.toString(exponent);
- } // else don't add zero exponent
- }
- if (truncated || negative && result.charAt(0) != '-') {
- result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
+ result = result.substring(0, result.length() - dropDigits);
+ lastDisplayedOffset[0] -= dropDigits;
+ }
+ result = result + "E" + Integer.toString(exponent);
}
return result;
}
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index b779a68..936d618 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -14,68 +14,6 @@
* limitations under the License.
*/
-//
-// This implements the calculator evaluation logic.
-// An evaluation is started with a call to evaluateAndShowResult().
-// This starts an asynchronous computation, which requests display
-// of the initial result, when available. When initial evaluation is
-// complete, it calls the calculator onEvaluate() method.
-// This occurs in a separate event, and may happen quite a bit
-// later. Once a result has been computed, and before the underlying
-// expression is modified, the getString method may be used to produce
-// Strings that represent approximations to various precisions.
-//
-// Actual expressions being evaluated are represented as CalculatorExprs,
-// which are just slightly preprocessed sequences of keypresses.
-//
-// The Evaluator owns the expression being edited and associated
-// state needed for evaluating it. It provides functionality for
-// saving and restoring this state. However the current
-// CalculatorExpr is exposed to the client, and may be directly modified
-// after cancelling any in-progress computations by invoking the
-// cancelAll() method.
-//
-// When evaluation is requested by the user, we invoke the eval
-// method on the CalculatorExpr from a background AsyncTask.
-// A subsequent getString() callback returns immediately, though it may
-// return a result containing placeholder '?' characters.
-// In that case we start a background task, which invokes the
-// onReevaluate() callback when it completes.
-// In both cases, the background task
-// computes the appropriate result digits by evaluating
-// the constructive real (CR) returned by CalculatorExpr.eval()
-// to the required precision.
-//
-// We cache the best approximation we have already computed.
-// We compute generously to allow for
-// some scrolling without recomputation and to minimize the chance of
-// digits flipping from "0000" to "9999". The best known
-// result approximation is maintained as a string by mCache (and
-// in a different format by the CR representation of the result).
-// When we are in danger of not having digits to display in response
-// to further scrolling, we initiate a background computation to higher
-// precision. If we actually do fall behind, we display placeholder
-// characters, e.g. blanks, and schedule a display update when the computation
-// completes.
-// The code is designed to ensure that the error in the displayed
-// result (excluding any placeholder characters) is always strictly less than 1 in
-// the last displayed digit. Typically we actually display a prefix
-// of a result that has this property and additionally is computed to
-// a significantly higher precision. Thus we almost always round correctly
-// towards zero. (Fully correct rounding towards zero is not computable.)
-//
-// Initial expression evaluation may time out. This may happen in the
-// case of domain errors such as division by zero, or for large computations.
-// We do not currently time out reevaluations to higher precision, since
-// the original evaluation prevcluded a domain error that could result
-// in non-termination. (We may discover that a presumed zero result is
-// actually slightly negative when re-evaluated; but that results in an
-// exception, which we can handle.) The user can abort either kind
-// of computation.
-//
-// We ensure that only one evaluation of either kind (AsyncReevaluator
-// or AsyncDisplayResult) is running at a time.
-
package com.android.calculator2;
import android.app.AlertDialog;
@@ -86,6 +24,7 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.hp.creals.CR;
@@ -100,102 +39,145 @@ import java.util.Date;
import java.util.Random;
import java.util.TimeZone;
+/**
+ * This implements the calculator evaluation logic. The underlying expression is constructed and
+ * edited with append(), delete(), and clear(). An evaluation an then be started with a call to
+ * evaluateAndShowResult() or requireResult(). This starts an asynchronous computation, which
+ * requests display of the initial result, when available. When initial evaluation is complete,
+ * it calls the calculator onEvaluate() method. This occurs in a separate event, possibly quite a
+ * bit later. Once a result has been computed, and before the underlying expression is modified,
+ * the getString() method may be used to produce Strings that represent approximations to various
+ * precisions.
+ *
+ * Actual expressions being evaluated are represented as {@link CalculatorExpr}s.
+ *
+ * The Evaluator owns the expression being edited and all associated state needed for evaluating
+ * it. It provides functionality for saving and restoring this state. However the current
+ * CalculatorExpr is exposed to the client, and may be directly accessed after cancelling any
+ * in-progress computations by invoking the cancelAll() method.
+ *
+ * When evaluation is requested, we invoke the eval() method on the CalculatorExpr from a
+ * background AsyncTask. A subsequent getString() callback returns immediately, though it may
+ * return a result containing placeholder ' ' characters. If we had to return palceholder
+ * characters, we start a background task, which invokes the onReevaluate() callback when it
+ * completes. In either case, the background task computes the appropriate result digits by
+ * evaluating the constructive real (CR) returned by CalculatorExpr.eval() to the required
+ * precision.
+ *
+ * We cache the best decimal approximation we have already computed. We compute generously to
+ * allow for some scrolling without recomputation and to minimize the chance of digits flipping
+ * from "0000" to "9999". The best known result approximation is maintained as a string by
+ * mResultString (and in a different format by the CR representation of the result). When we are
+ * in danger of not having digits to display in response to further scrolling, we also initiate a
+ * background computation to higher precision, as if we had generated placeholder characters.
+ *
+ * The code is designed to ensure that the error in the displayed result (excluding any
+ * placeholder characters) is always strictly less than 1 in the last displayed digit. Typically
+ * we actually display a prefix of a result that has this property and additionally is computed to
+ * a significantly higher precision. Thus we almost always round correctly towards zero. (Fully
+ * correct rounding towards zero is not computable, at least given our representation.)
+ *
+ * Initial expression evaluation may time out. This may happen in the case of domain errors such
+ * as division by zero, or for large computations. We do not currently time out reevaluations to
+ * higher precision, since the original evaluation precluded a domain error that could result in
+ * non-termination. (We may discover that a presumed zero result is actually slightly negative
+ * when re-evaluated; but that results in an exception, which we can handle.) The user can abort
+ * either kind of computation.
+ *
+ * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is
+ * running at a time.
+ */
class Evaluator {
- private static final String KEY_PREF_DEGREE_MODE = "degree_mode";
+ // When naming variables and fields, "Offset" denotes a character offset in a string
+ // representing a decimal number, where the offset is relative to the decimal point. 1 =
+ // tenths position, -1 = units position. Integer.MAX_VALUE is sometimes used for the offset
+ // of the last digit in an a nonterminating decimal expansion. We use the suffix "Index" to
+ // denote a zero-based absolute index into such a string.
- private final Calculator mCalculator;
- private final CalculatorResult mResult; // The result display View
- private CalculatorExpr mExpr; // Current calculator expression
- private CalculatorExpr mSaved; // Last saved expression.
- // Either null or contains a single
- // preevaluated node.
- private String mSavedName; // A hopefully unique name associated
- // with mSaved.
- // The following are valid only if an evaluation
- // completed successfully.
- private CR mVal; // value of mExpr as constructive real
- private BoundedRational mRatVal; // value of mExpr as rational or null
- private int mLastDigs; // Last digit argument passed to getString()
- // for this result, or the initial preferred
- // precision.
- private boolean mDegreeMode; // Currently in degree (not radian) mode
- private final Handler mTimeoutHandler;
-
- static final BigInteger BIG_MILLION = BigInteger.valueOf(1000000);
+ private static final String KEY_PREF_DEGREE_MODE = "degree_mode";
+ // The minimum number of extra digits we always try to compute to improve the chance of
+ // producing a correctly-rounded-towards-zero result. The extra digits can be displayed to
+ // avoid generating placeholder digits, but should only be displayed briefly while computing.
private static final int EXTRA_DIGITS = 20;
- // Extra computed digits to minimize probably we will have
- // to change our minds about digits we already displayed.
- // (The correct digits are technically not computable using our
- // representation: An off by one error in the last digits
- // can affect earlier ones, even though the display is
- // always within one in the lsd. This is only visible
- // for results that end in EXTRA_DIGITS 9s or 0s, but are
- // not integers.)
- // We do use these extra digits to display while we are
- // computing the correct answer. Thus they may be
- // temporarily visible.
- private static final int EXTRA_DIVISOR = 5;
- // We add the length of the previous result divided by
- // EXTRA_DIVISOR to try to recover recompute latency when
- // scrolling through a long result.
- private static final int PRECOMPUTE_DIGITS = 30;
- private static final int PRECOMPUTE_DIVISOR = 5;
- // When we have to reevaluate, we compute an extra
- // PRECOMPUTE_DIGITS
- // + <current_result_length>/PRECOMPUTE_DIVISOR digits.
- // The last term is dropped if prec < 0.
-
- // We cache the result as a string to accelerate scrolling.
- // The cache is filled in by the UI thread, but this may
- // happen asynchronously, much later than the request.
- private String mCache; // Current best known result, which includes
- private int mCacheDigs = 0; // mCacheDigs digits to the right of the
- // decimal point. Always positive.
- // mCache is valid when non-null
- // unless the expression has been
- // changed since the last evaluation call.
- private int mCacheDigsReq; // Number of digits that have been
- // requested. Only touched by UI
- // thread.
- public static final int INVALID_MSD = Integer.MAX_VALUE;
- private int mMsd = INVALID_MSD; // Position of most significant digit
- // in current cached result, if determined.
- // This is just the index in mCache
- // holding the msd.
+
+ // We adjust EXTRA_DIGITS by adding the length of the previous result divided by
+ // EXTRA_DIVISOR. This helps hide recompute latency when long results are requested;
+ // We start the recomputation substantially before the need is likely to be visible.
+ private static final int EXTRA_DIVISOR = 5;
+
+ // In addition to insisting on extra digits (see above), we minimize reevaluation
+ // frequency by precomputing an extra PRECOMPUTE_DIGITS
+ // + <current_precision_offset>/PRECOMPUTE_DIVISOR digits, whenever we are forced to
+ // reevaluate. The last term is dropped if prec < 0.
+ private static final int PRECOMPUTE_DIGITS = 30;
+ private static final int PRECOMPUTE_DIVISOR = 5;
+
+ // Initial evaluation precision. Enough to guarantee that we can compute the short
+ // representation, and that we rarely have to evaluate nonzero results to MAX_MSD_PREC_OFFSET.
+ // It also helps if this is at least EXTRA_DIGITS + display width, so that we don't
+ // immediately need a second evaluation.
private static final int INIT_PREC = 50;
- // Initial evaluation precision. Enough to guarantee
- // that we can compute the short representation, and that
- // we rarely have to evaluate nonzero results to
- // MAX_MSD_PREC. It also helps if this is at least
- // EXTRA_DIGITS + display width, so that we don't
- // immediately need a second evaluation.
- private static final int MAX_MSD_PREC = 320;
- // The largest number of digits to the right
- // of the decimal point to which we will
- // evaluate to compute proper scientific
- // notation for values close to zero.
- // Chosen to ensure that we always to better than
- // IEEE double precision at identifying nonzeros.
+
+ // The largest number of digits to the right of the decimal point to which we will evaluate to
+ // compute proper scientific notation for values close to zero. Chosen to ensure that we
+ // always to better than IEEE double precision at identifying nonzeros.
+ private static final int MAX_MSD_PREC_OFFSET = 320;
+
+ // If we can replace an exponent by this many leading zeroes, we do so. Also used in
+ // estimating exponent size for truncating short representation.
private static final int EXP_COST = 3;
- // If we can replace an exponent by this many leading zeroes,
- // we do so. Also used in estimating exponent size for
- // truncating short representation.
- private AsyncReevaluator mCurrentReevaluator;
- // The one and only un-cancelled and currently running reevaluator.
- // Touched only by UI thread.
+ private final Calculator mCalculator;
+ private final CalculatorResult mResult;
+
+ // The current caluclator expression.
+ private CalculatorExpr mExpr;
- private AsyncDisplayResult mEvaluator;
- // Currently running expression evaluator, if any.
+ // Last saved expression. Either null or contains a single CalculatorExpr.PreEval node.
+ private CalculatorExpr mSaved;
+ // A hopefully unique name associated with mSaved.
+ private String mSavedName;
+
+ // The expression may have changed since the last evaluation in ways that would affect its
+ // value.
private boolean mChangedValue;
- // The expression may have changed since the last evaluation in ways that would
- // affect its value.
private SharedPreferences mSharedPrefs;
+ private boolean mDegreeMode; // Currently in degree (not radian) mode.
+
+ private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts.
+
+ // The following are valid only if an evaluation completed successfully.
+ private CR mVal; // Value of mExpr as constructive real.
+ private BoundedRational mRatVal; // Value of mExpr as rational or null.
+
+ // We cache the best known decimal result in mResultString. Whenever that is
+ // non-null, it is computed to exactly mResultStringOffset, which is always > 0.
+ // The cache is filled in by the UI thread.
+ // Valid only if mResultString is non-null and !mChangedValue.
+ private String mResultString;
+ private int mResultStringOffset = 0;
+
+ // Number of digits to which (possibly incomplete) evaluation has been requested.
+ // Only accessed by UI thread.
+ private int mResultStringOffsetReq; // Number of digits that have been
+
+ public static final int INVALID_MSD = Integer.MAX_VALUE;
+
+ // Position of most significant digit in current cached result, if determined. This is just
+ // the index in mResultString holding the msd.
+ private int mMsdIndex = INVALID_MSD;
+
+ // Currently running expression evaluator, if any.
+ private AsyncEvaluator mEvaluator;
+
+ // The one and only un-cancelled and currently running reevaluator. Touched only by UI thread.
+ private AsyncReevaluator mCurrentReevaluator;
+
Evaluator(Calculator calculator,
CalculatorResult resultDisplay) {
mCalculator = calculator;
@@ -209,86 +191,36 @@ class Evaluator {
mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
}
- // Result of asynchronous reevaluation
- class ReevalResult {
- ReevalResult(String s, int p) {
- mNewCache = s;
- mNewCacheDigs = p;
- }
- final String mNewCache;
- final int mNewCacheDigs;
- }
-
- // Compute new cache contents accurate to prec digits to the right
- // of the decimal point. Ensure that redisplay() is called after
- // doing so. If the evaluation fails for reasons other than a
- // timeout, ensure that DisplayError() is called.
- class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
- @Override
- protected ReevalResult doInBackground(Integer... prec) {
- try {
- int eval_prec = prec[0].intValue();
- return new ReevalResult(mVal.toString(eval_prec), eval_prec);
- } catch(ArithmeticException e) {
- return null;
- } catch(CR.PrecisionOverflowException e) {
- return null;
- } catch(CR.AbortedException e) {
- // Should only happen if the task was cancelled,
- // in which case we don't look at the result.
- return null;
- }
- }
- @Override
- protected void onPostExecute(ReevalResult result) {
- if (result == null) {
- // This should only be possible in the extremely rare
- // case of encountering a domain error while reevaluating
- // or in case of a precision overflow. We don't know of
- // a way to get the latter with a plausible amount of
- // user input.
- mCalculator.onError(R.string.error_nan);
- } else {
- if (result.mNewCacheDigs < mCacheDigs) {
- throw new AssertionError("Unexpected onPostExecute timing");
- }
- mCache = result.mNewCache;
- mCacheDigs = result.mNewCacheDigs;
- mCalculator.onReevaluate();
- }
- mCurrentReevaluator = null;
- }
- // On cancellation we do nothing; invoker should have
- // left no trace of us.
- }
-
- // Result of initial asynchronous computation
+ /**
+ * Result of initial asynchronous result computation.
+ * Represents either an error or a result computed to an initial evaluation precision.
+ */
private static class InitialResult {
- InitialResult(CR val, BoundedRational ratVal, String s, int p, int idp) {
- mErrorResourceId = Calculator.INVALID_RES_ID;
- mVal = val;
- mRatVal = ratVal;
- mNewCache = s;
- mNewCacheDigs = p;
- mInitDisplayPrec = idp;
+ public final int errorResourceId; // Error string or INVALID_RES_ID.
+ public final CR val; // Constructive real value.
+ public final BoundedRational ratVal; // Rational value or null.
+ public final String newResultString; // Null iff it can't be computed.
+ public final int newResultStringOffset;
+ public final int initDisplayOffset;
+ InitialResult(CR v, BoundedRational rv, String s, int p, int idp) {
+ errorResourceId = Calculator.INVALID_RES_ID;
+ val = v;
+ ratVal = rv;
+ newResultString = s;
+ newResultStringOffset = p;
+ initDisplayOffset = idp;
}
- InitialResult(int errorResourceId) {
- mErrorResourceId = errorResourceId;
- mVal = CR.valueOf(0);
- mRatVal = BoundedRational.ZERO;
- mNewCache = "BAD";
- mNewCacheDigs = 0;
- mInitDisplayPrec = 0;
+ InitialResult(int errorId) {
+ errorResourceId = errorId;
+ val = CR.valueOf(0);
+ ratVal = BoundedRational.ZERO;
+ newResultString = "BAD";
+ newResultStringOffset = 0;
+ initDisplayOffset = 0;
}
boolean isError() {
- return mErrorResourceId != Calculator.INVALID_RES_ID;
+ return errorResourceId != Calculator.INVALID_RES_ID;
}
- final int mErrorResourceId;
- final CR mVal;
- final BoundedRational mRatVal;
- final String mNewCache; // Null iff it can't be computed.
- final int mNewCacheDigs;
- final int mInitDisplayPrec;
}
private void displayCancelledMessage() {
@@ -302,52 +234,71 @@ class Evaluator {
.show();
}
- private final long MAX_TIMEOUT = 15000;
- // Milliseconds.
- // Longer is unlikely to help unless
- // we get more heap space.
- private long mTimeout = 2000; // Timeout for requested evaluations,
- // in milliseconds.
- // This is currently not saved and restored
- // with the state; we reset
- // the timeout when the
- // calculator is restarted.
- // We'll call that a feature; others
- // might argue it's a bug.
+ // Timeout handling.
+ // Expressions are evaluated with a sort timeout or a long timeout.
+ // Each implies different maxima on both computation time and bit length.
+ // We recheck bit length separetly to avoid wasting time on decimal conversions that are
+ // destined to fail.
+
+ /**
+ * Is a long timeout in effect for the main expression?
+ */
+ private boolean mLongTimeout = false;
+
+ /**
+ * Is a long timeout in effect for the saved expression?
+ */
+ private boolean mLongSavedTimeout = false;
+
+ /**
+ * Return the timeout in milliseconds.
+ * @param longTimeout a long timeout is in effect
+ */
+ private long getTimeout(boolean longTimeout) {
+ return longTimeout ? 15000 : 2000;
+ // Exceeding a few tens of seconds increases the risk of running out of memory
+ // and impacting the rest of the system.
+ }
+
+ /**
+ * Return the maximum number of bits in the result. Longer results are assumed to time out.
+ * @param longTimeout a long timeout is in effect
+ */
+ private int getMaxResultBits(boolean longTimeout) {
+ return longTimeout ? 350000 : 120000;
+ }
+
+ /**
+ * Timeout for unrequested, speculative evaluations, in milliseconds.
+ */
private final long QUICK_TIMEOUT = 1000;
- // Timeout for unrequested, speculative
- // evaluations, in milliseconds.
- private int mMaxResultBits = 120000; // Don't try to display a larger result.
- private final int MAX_MAX_RESULT_BITS = 350000; // Long timeout version.
- private final int QUICK_MAX_RESULT_BITS = 50000; // Instant result version.
+
+ /**
+ * Maximum result bit length for unrequested, speculative evaluations.
+ */
+ private final int QUICK_MAX_RESULT_BITS = 50000;
private void displayTimeoutMessage() {
- final AlertDialog.Builder builder = new AlertDialog.Builder(mCalculator)
- .setMessage(R.string.timeout)
- .setNegativeButton(R.string.dismiss, null /* listener */);
- if (mTimeout != MAX_TIMEOUT) {
- builder.setPositiveButton(R.string.ok_remove_timeout,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface d, int which) {
- mTimeout = MAX_TIMEOUT;
- mMaxResultBits = MAX_MAX_RESULT_BITS;
- }
- });
- }
- builder.show();
+ AlertDialogFragment.showMessageDialog(mCalculator, mCalculator.getString(R.string.timeout),
+ (mLongTimeout ? null : mCalculator.getString(R.string.ok_remove_timeout)));
+ }
+
+ public void setLongTimeOut() {
+ mLongTimeout = true;
}
- // Compute initial cache contents and result when we're good and ready.
- // We leave the expression display up, with scrolling
- // disabled, until this computation completes.
- // Can result in an error display if something goes wrong.
- // By default we set a timeout to catch runaway computations.
- class AsyncDisplayResult extends AsyncTask<Void, Void, InitialResult> {
+ /**
+ * Compute initial cache contents and result when we're good and ready.
+ * We leave the expression display up, with scrolling disabled, until this computation
+ * completes. Can result in an error display if something goes wrong. By default we set a
+ * timeout to catch runaway computations.
+ */
+ class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> {
private boolean mDm; // degrees
private boolean mRequired; // Result was requested by user.
private boolean mQuiet; // Suppress cancellation message.
private Runnable mTimeoutRunnable = null;
- AsyncDisplayResult(boolean dm, boolean required) {
+ AsyncEvaluator(boolean dm, boolean required) {
mDm = dm;
mRequired = required;
mQuiet = !required;
@@ -370,7 +321,7 @@ class Evaluator {
}
@Override
protected void onPreExecute() {
- long timeout = mRequired ? mTimeout : QUICK_TIMEOUT;
+ long timeout = mRequired ? getTimeout(mLongTimeout) : QUICK_TIMEOUT;
mTimeoutRunnable = new Runnable() {
@Override
public void run() {
@@ -379,12 +330,15 @@ class Evaluator {
};
mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
}
+ /**
+ * Is a computed result too big for decimal conversion?
+ */
private boolean isTooBig(CalculatorExpr.EvalResult res) {
- int maxBits = mRequired ? mMaxResultBits : QUICK_MAX_RESULT_BITS;
- if (res.mRatVal != null) {
- return res.mRatVal.wholeNumberBits() > maxBits;
+ int maxBits = mRequired ? getMaxResultBits(mLongTimeout) : QUICK_MAX_RESULT_BITS;
+ if (res.ratVal != null) {
+ return res.ratVal.wholeNumberBits() > maxBits;
} else {
- return res.mVal.get_appr(maxBits).bitLength() > 2;
+ return res.val.get_appr(maxBits).bitLength() > 2;
}
}
@Override
@@ -395,35 +349,33 @@ class Evaluator {
// Avoid starting a long uninterruptible decimal conversion.
return new InitialResult(R.string.timeout);
}
- int prec = INIT_PREC;
- String initCache = res.mVal.toString(prec);
- int msd = getMsdPos(initCache);
- if (BoundedRational.asBigInteger(res.mRatVal) == null
+ int precOffset = INIT_PREC;
+ String initResult = res.val.toString(precOffset);
+ int msd = getMsdIndexOf(initResult);
+ if (BoundedRational.asBigInteger(res.ratVal) == null
&& msd == INVALID_MSD) {
- prec = MAX_MSD_PREC;
- initCache = res.mVal.toString(prec);
- msd = getMsdPos(initCache);
+ precOffset = MAX_MSD_PREC_OFFSET;
+ initResult = res.val.toString(precOffset);
+ msd = getMsdIndexOf(initResult);
}
- int lsd = getLsd(res.mRatVal, initCache, initCache.indexOf('.'));
- int initDisplayPrec = getPreferredPrec(initCache, msd, lsd);
- int newPrec = initDisplayPrec + EXTRA_DIGITS;
- if (newPrec > prec) {
- prec = newPrec;
- initCache = res.mVal.toString(prec);
+ final int lsdOffset = getLsdOffset(res.ratVal, initResult,
+ initResult.indexOf('.'));
+ final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset);
+ final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS;
+ if (newPrecOffset > precOffset) {
+ precOffset = newPrecOffset;
+ initResult = res.val.toString(precOffset);
}
- return new InitialResult(res.mVal, res.mRatVal,
- initCache, prec, initDisplayPrec);
+ return new InitialResult(res.val, res.ratVal,
+ initResult, precOffset, initDisplayOffset);
} catch (CalculatorExpr.SyntaxException e) {
return new InitialResult(R.string.error_syntax);
} catch (BoundedRational.ZeroDivisionException e) {
- // Division by zero caught by BoundedRational;
- // the easy and more common case.
return new InitialResult(R.string.error_zero_divide);
} catch(ArithmeticException e) {
return new InitialResult(R.string.error_nan);
} catch(CR.PrecisionOverflowException e) {
- // Extremely unlikely unless we're actually dividing by
- // zero or the like.
+ // Extremely unlikely unless we're actually dividing by zero or the like.
return new InitialResult(R.string.error_overflow);
} catch(CR.AbortedException e) {
return new InitialResult(R.string.error_aborted);
@@ -434,41 +386,41 @@ class Evaluator {
mEvaluator = null;
mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
if (result.isError()) {
- if (result.mErrorResourceId == R.string.timeout) {
+ if (result.errorResourceId == R.string.timeout) {
if (mRequired) {
displayTimeoutMessage();
}
mCalculator.onCancelled();
} else {
- mCalculator.onError(result.mErrorResourceId);
+ mCalculator.onError(result.errorResourceId);
}
return;
}
- mVal = result.mVal;
- mRatVal = result.mRatVal;
- mCache = result.mNewCache;
- mCacheDigs = result.mNewCacheDigs;
- mLastDigs = result.mInitDisplayPrec;
- int dotPos = mCache.indexOf('.');
- String truncatedWholePart = mCache.substring(0, dotPos);
- // Recheck display precision; it may change, since
- // display dimensions may have been unknow the first time.
- // In that case the initial evaluation precision should have
+ mVal = result.val;
+ mRatVal = result.ratVal;
+ // TODO: If the new result ends in lots of zeroes, and we have a rational result which
+ // is greater than (in absolute value) the result string, we should subtract 1 ulp
+ // from the result string. That will prevent a later change from zeroes to nines. We
+ // know that the correct, rounded-toward-zero result has nines.
+ mResultString = result.newResultString;
+ mResultStringOffset = result.newResultStringOffset;
+ final int dotIndex = mResultString.indexOf('.');
+ String truncatedWholePart = mResultString.substring(0, dotIndex);
+ // Recheck display precision; it may change, since display dimensions may have been
+ // unknow the first time. In that case the initial evaluation precision should have
// been conservative.
- // TODO: Could optimize by remembering display size and
- // checking for change.
- int init_prec = result.mInitDisplayPrec;
- int msd = getMsdPos(mCache);
- int leastDigPos = getLsd(mRatVal, mCache, dotPos);
- int new_init_prec = getPreferredPrec(mCache, msd, leastDigPos);
- if (new_init_prec < init_prec) {
- init_prec = new_init_prec;
+ // TODO: Could optimize by remembering display size and checking for change.
+ int initPrecOffset = result.initDisplayOffset;
+ final int msdIndex = getMsdIndexOf(mResultString);
+ final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
+ final int newInitPrecOffset = getPreferredPrec(mResultString, msdIndex, leastDigOffset);
+ if (newInitPrecOffset < initPrecOffset) {
+ initPrecOffset = newInitPrecOffset;
} else {
- // They should be equal. But nothing horrible should
- // happen if they're not. e.g. because
- // CalculatorResult.MAX_WIDTH was too small.
+ // They should be equal. But nothing horrible should happen if they're not. e.g.
+ // because CalculatorResult.MAX_WIDTH was too small.
}
- mCalculator.onEvaluate(init_prec, msd, leastDigPos, truncatedWholePart);
+ mCalculator.onEvaluate(initPrecOffset, msdIndex, leastDigOffset, truncatedWholePart);
}
@Override
protected void onCancelled(InitialResult result) {
@@ -476,82 +428,177 @@ class Evaluator {
mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
if (mRequired && !mQuiet) {
displayCancelledMessage();
- } // Otherwise timeout processing displayed message.
+ } // Otherwise, if mRequired, timeout processing displayed message.
mCalculator.onCancelled();
// Just drop the evaluation; Leave expression displayed.
return;
}
}
+ /**
+ * Check whether a new higher precision result flips previously computed trailing 9s
+ * to zeroes. If so, flip them back. Return the adjusted result.
+ * Assumes newPrecOffset >= oldPrecOffset > 0.
+ * Since our results are accurate to < 1 ulp, this can only happen if the true result
+ * is less than the new result with trailing zeroes, and thus appending 9s to the
+ * old result must also be correct. Such flips are impossible if the newly computed
+ * digits consist of anything other than zeroes.
+ * It is unclear that there are real cases in which this is necessary,
+ * but we have failed to prove there aren't such cases.
+ */
+ @VisibleForTesting
+ static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
+ int newPrecOffset) {
+ final int oldLen = oldDigs.length();
+ if (oldDigs.charAt(oldLen - 1) != '9') {
+ return newDigs;
+ }
+ final int newLen = newDigs.length();
+ final int precDiff = newPrecOffset - oldPrecOffset;
+ final int oldLastInNew = newLen - 1 - precDiff;
+ if (newDigs.charAt(oldLastInNew) != '0') {
+ return newDigs;
+ }
+ // Earlier digits could not have changed without a 0 to 9 or 9 to 0 flip at end.
+ // The former is OK.
+ if (!newDigs.substring(newLen - precDiff).equals(repeat('0', precDiff))) {
+ throw new AssertionError("New approximation invalidates old one!");
+ }
+ return oldDigs + repeat('9', precDiff);
+ }
- // Start an evaluation to prec, and ensure that the
- // display is redrawn when it completes.
- private void ensureCachePrec(int prec) {
- if (mCache != null && mCacheDigs >= prec
- || mCacheDigsReq >= prec) return;
+ /**
+ * Result of asynchronous reevaluation.
+ */
+ private static class ReevalResult {
+ public final String newResultString;
+ public final int newResultStringOffset;
+ ReevalResult(String s, int p) {
+ newResultString = s;
+ newResultStringOffset = p;
+ }
+ }
+
+ /**
+ * Compute new mResultString contents to prec digits to the right of the decimal point.
+ * Ensure that onReevaluate() is called after doing so. If the evaluation fails for reasons
+ * other than a timeout, ensure that onError() is called.
+ */
+ private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
+ @Override
+ protected ReevalResult doInBackground(Integer... prec) {
+ try {
+ final int precOffset = prec[0].intValue();
+ return new ReevalResult(mVal.toString(precOffset), precOffset);
+ } catch(ArithmeticException e) {
+ return null;
+ } catch(CR.PrecisionOverflowException e) {
+ return null;
+ } catch(CR.AbortedException e) {
+ // Should only happen if the task was cancelled, in which case we don't look at
+ // the result.
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(ReevalResult result) {
+ if (result == null) {
+ // This should only be possible in the extremely rare case of encountering a
+ // domain error while reevaluating or in case of a precision overflow. We don't
+ // know of a way to get the latter with a plausible amount of user input.
+ mCalculator.onError(R.string.error_nan);
+ } else {
+ if (result.newResultStringOffset < mResultStringOffset) {
+ throw new AssertionError("Unexpected onPostExecute timing");
+ }
+ mResultString = unflipZeroes(mResultString, mResultStringOffset,
+ result.newResultString, result.newResultStringOffset);
+ mResultStringOffset = result.newResultStringOffset;
+ mCalculator.onReevaluate();
+ }
+ mCurrentReevaluator = null;
+ }
+ // On cancellation we do nothing; invoker should have left no trace of us.
+ }
+
+ /**
+ * If necessary, start an evaluation to precOffset.
+ * Ensure that the display is redrawn when it completes.
+ */
+ private void ensureCachePrec(int precOffset) {
+ if (mResultString != null && mResultStringOffset >= precOffset
+ || mResultStringOffsetReq >= precOffset) return;
if (mCurrentReevaluator != null) {
// Ensure we only have one evaluation running at a time.
mCurrentReevaluator.cancel(true);
mCurrentReevaluator = null;
}
mCurrentReevaluator = new AsyncReevaluator();
- mCacheDigsReq = prec + PRECOMPUTE_DIGITS;
- if (mCache != null) {
- mCacheDigsReq += mCacheDigsReq / PRECOMPUTE_DIVISOR;
+ mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
+ if (mResultString != null) {
+ mResultStringOffsetReq += mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
}
- mCurrentReevaluator.execute(mCacheDigsReq);
+ mCurrentReevaluator.execute(mResultStringOffsetReq);
}
/**
* Return the rightmost nonzero digit position, if any.
* @param ratVal Rational value of result or null.
* @param cache Current cached decimal string representation of result.
- * @param decPos Index of decimal point in cache.
+ * @param decIndex Index of decimal point in cache.
* @result Position of rightmost nonzero digit relative to decimal point.
* Integer.MIN_VALUE if ratVal is zero. Integer.MAX_VALUE if there is no lsd,
* or we cannot determine it.
*/
- int getLsd(BoundedRational ratVal, String cache, int decPos) {
+ int getLsdOffset(BoundedRational ratVal, String cache, int decIndex) {
if (ratVal != null && ratVal.signum() == 0) return Integer.MIN_VALUE;
int result = BoundedRational.digitsRequired(ratVal);
if (result == 0) {
int i;
- for (i = -1; decPos + i > 0 && cache.charAt(decPos + i) == '0'; --i) { }
+ for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { }
result = i;
}
return result;
}
+ // TODO: We may want to consistently specify the position of the current result
+ // window using the left-most visible digit index instead of the offset for the rightmost one.
+ // It seems likely that would simplify the logic.
+
/**
- * Retrieve the preferred precision for the currently displayed result.
+ * Retrieve the preferred precision "offset" for the currently displayed result.
* May be called from non-UI thread.
* @param cache Current approximation as string.
* @param msd Position of most significant digit in result. Index in cache.
* Can be INVALID_MSD if we haven't found it yet.
- * @param lastDigit Position of least significant digit (1 = tenths digit)
+ * @param lastDigitOffset Position of least significant digit (1 = tenths digit)
* or Integer.MAX_VALUE.
*/
- int getPreferredPrec(String cache, int msd, int lastDigit) {
- int lineLength = mResult.getMaxChars();
- int wholeSize = cache.indexOf('.');
- int negative = cache.charAt(0) == '-' ? 1 : 0;
+ private int getPreferredPrec(String cache, int msd, int lastDigitOffset) {
+ final int lineLength = mResult.getMaxChars();
+ final int wholeSize = cache.indexOf('.');
+ final int negative = cache.charAt(0) == '-' ? 1 : 0;
// Don't display decimal point if result is an integer.
- if (lastDigit == 0) lastDigit = -1;
- if (lastDigit != Integer.MAX_VALUE) {
- if (wholeSize <= lineLength && lastDigit <= 0) {
+ if (lastDigitOffset == 0) {
+ lastDigitOffset = -1;
+ }
+ if (lastDigitOffset != Integer.MAX_VALUE) {
+ if (wholeSize <= lineLength && lastDigitOffset <= 0) {
// Exact integer. Prefer to display as integer, without decimal point.
return -1;
}
- if (lastDigit >= 0 && wholeSize + lastDigit + 1 /* dec.pt. */ <= lineLength) {
+ if (lastDigitOffset >= 0
+ && wholeSize + lastDigitOffset + 1 /* decimal pt. */ <= lineLength) {
// Display full exact number wo scientific notation.
- return lastDigit;
+ return lastDigitOffset;
}
}
if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) {
// Display number without scientific notation. Treat leading zero as msd.
msd = wholeSize - 1;
}
- if (msd > wholeSize + MAX_MSD_PREC) {
+ if (msd > wholeSize + MAX_MSD_PREC_OFFSET) {
// Display a probable but uncertain 0 as "0.000000000",
// without exponent. That's a judgment call, but less likely
// to confuse naive users. A more informative and confusing
@@ -576,10 +623,10 @@ class Evaluator {
* that if it doesn't contain enough significant digits, we can
* reasonably abbreviate as SHORT_UNCERTAIN_ZERO.
* @param msdIndex Index of most significant digit in cache, or INVALID_MSD.
- * @param lsd Position of least significant digit in finite representation,
+ * @param lsdOffset Position of least significant digit in finite representation,
* relative to decimal point, or MAX_VALUE.
*/
- private String getShortString(String cache, int msdIndex, int lsd) {
+ private String getShortString(String cache, int msdIndex, int lsdOffset) {
// This somewhat mirrors the display formatting code, but
// - The constants are different, since we don't want to use the whole display.
// - This is an easier problem, since we don't support scrolling and the length
@@ -594,7 +641,7 @@ class Evaluator {
msdIndex = INVALID_MSD;
}
if (msdIndex == INVALID_MSD) {
- if (lsd < INIT_PREC) {
+ if (lsdOffset < INIT_PREC) {
return "0";
} else {
return SHORT_UNCERTAIN_ZERO;
@@ -602,19 +649,19 @@ class Evaluator {
}
// Avoid scientific notation for small numbers of zeros.
// Instead stretch significant digits to include decimal point.
- if (lsd < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH
- && lsd >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) {
+ if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH
+ && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) {
// Whole number that fits in allotted space.
// CalculatorResult would not use scientific notation either.
- lsd = -1;
+ lsdOffset = -1;
}
if (msdIndex > dotIndex) {
if (msdIndex <= dotIndex + EXP_COST + 1) {
// Preferred display format inthis cases is with leading zeroes, even if
// it doesn't fit entirely. Replicate that here.
msdIndex = dotIndex - 1;
- } else if (lsd <= SHORT_TARGET_LENGTH - negative - 2
- && lsd <= CalculatorResult.MAX_LEADING_ZEROES + 1) {
+ } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2
+ && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) {
// Fraction that fits entirely in allotted space.
// CalculatorResult would not use scientific notation either.
msdIndex = dotIndex -1;
@@ -625,10 +672,10 @@ class Evaluator {
// Adjust for the fact that the decimal point itself takes space.
exponent--;
}
- if (lsd != Integer.MAX_VALUE) {
- int lsdIndex = dotIndex + lsd;
- int totalDigits = lsdIndex - msdIndex + negative + 1;
- if (totalDigits <= SHORT_TARGET_LENGTH && dotIndex > msdIndex && lsd >= -1) {
+ if (lsdOffset != Integer.MAX_VALUE) {
+ final int lsdIndex = dotIndex + lsdOffset;
+ final int totalDigits = lsdIndex - msdIndex + negative + 1;
+ if (totalDigits <= SHORT_TARGET_LENGTH && dotIndex > msdIndex && lsdOffset >= -1) {
// Fits, no exponent needed.
return negativeSign + cache.substring(msdIndex, lsdIndex + 1);
}
@@ -648,183 +695,182 @@ class Evaluator {
+ KeyMaps.ELLIPSIS + "E" + exponent;
}
- // Return the most significant digit position in the given string
- // or INVALID_MSD.
- public static int getMsdPos(String s) {
- int len = s.length();
- int nonzeroPos = -1;
+ /**
+ * Return the most significant digit index in the given numeric string.
+ * Return INVALID_MSD if there are not enough digits to prove the numeric value is
+ * different from zero. As usual, we assume an error of strictly less than 1 ulp.
+ */
+ public static int getMsdIndexOf(String s) {
+ final int len = s.length();
+ int nonzeroIndex = -1;
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
if (c != '-' && c != '.' && c != '0') {
- nonzeroPos = i;
+ nonzeroIndex = i;
break;
}
}
- if (nonzeroPos >= 0 &&
- (nonzeroPos < len - 1 || s.charAt(nonzeroPos) != '1')) {
- return nonzeroPos;
+ if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) {
+ return nonzeroIndex;
} else {
- // Unknown, or could change on reevaluation
return INVALID_MSD;
}
}
- // Return most significant digit position in the cache, if determined,
- // INVALID_MSD ow.
- // If unknown, and we've computed less than DESIRED_PREC,
- // schedule reevaluation and redisplay, with higher precision.
- int getMsd() {
- if (mMsd != INVALID_MSD) return mMsd;
+ /**
+ * Return most significant digit index in the currently computed result.
+ * Returns an index in the result character array. Return INVALID_MSD if the current result
+ * is too close to zero to determine the result.
+ * Result is almost consistent through reevaluations: It may increase by one, once.
+ */
+ private int getMsdIndex() {
+ if (mMsdIndex != INVALID_MSD) {
+ // 0.100000... can change to 0.0999999... We may have to correct once by one digit.
+ if (mResultString.charAt(mMsdIndex) == '0') {
+ mMsdIndex++;
+ }
+ return mMsdIndex;
+ }
if (mRatVal != null && mRatVal.signum() == 0) {
return INVALID_MSD; // None exists
}
- int res = INVALID_MSD;
- if (mCache != null) {
- res = getMsdPos(mCache);
+ int result = INVALID_MSD;
+ if (mResultString != null) {
+ result = getMsdIndexOf(mResultString);
}
- if (res == INVALID_MSD && mEvaluator == null
- && mCurrentReevaluator == null && mCacheDigs < MAX_MSD_PREC) {
- // We assert that mCache is not null, since there is no
- // evaluator running.
- ensureCachePrec(MAX_MSD_PREC);
- // Could reevaluate more incrementally, but we suspect that if
- // we have to reevaluate at all, the result is probably zero.
- }
- return res;
+ return result;
}
- // Return a string with n placeholder characters.
- private String getPadding(int n) {
- StringBuilder padding = new StringBuilder();
+ /**
+ * Return a string with n copies of c.
+ */
+ private static String repeat(char c, int n) {
+ StringBuilder result = new StringBuilder();
for (int i = 0; i < n; ++i) {
- padding.append(' '); // To be replaced during final translation.
+ result.append(c);
}
- return padding.toString();
+ return result.toString();
}
- // Return the number of zero characters at the beginning of s
- private int leadingZeroes(String s) {
- int res = 0;
- int len = s.length();
- for (res = 0; res < len && s.charAt(res) == '0'; ++res) {}
- return res;
- }
+ // Refuse to scroll past the point at which this many digits from the whole number
+ // part of the result are still displayed. Avoids sily displays like 1E1.
+ private static final int MIN_DISPLAYED_DIGS = 5;
- private static final int MIN_DIGS = 5;
- // Leave at least this many digits from the whole number
- // part on the screen, to avoid silly displays like 1E1.
- // Return result to exactly prec[0] digits to the right of the
- // decimal point.
- // The result should be no longer than maxDigs.
- // No exponent or other indication of precision is added.
- // The result is returned immediately, based on the
- // current cache contents, but it may contain question
- // marks for unknown digits. It may also use uncertain
- // digits within EXTRA_DIGITS. If either of those occurred,
- // schedule a reevaluation and redisplay operation.
- // Uncertain digits never appear to the left of the decimal point.
- // digs may be negative to only retrieve digits to the left
- // of the decimal point. (prec[0] = 0 means we include
- // the decimal point, but nothing to the right. prec[0] = -1
- // means we drop the decimal point and start at the ones
- // position. Should not be invoked if mVal is null.
- // This essentially just returns a substring of the full result;
- // a leading minus sign or leading digits can be dropped.
- // Result uses US conventions; is NOT internationalized.
- // We set negative[0] if the number as a whole is negative,
- // since we may drop the minus sign.
- // We set truncated[0] if leading nonzero digits were dropped.
- // getRational() can be used to determine whether the result
- // is exact, or whether we dropped trailing digits.
- // If the requested prec[0] value is out of range, we update
- // it in place and use the updated value. But we do not make it
- // greater than maxPrec.
- public String getString(int[] prec, int maxPrec, int maxDigs,
- boolean[] truncated, boolean[] negative) {
- int digs = prec[0];
- mLastDigs = digs;
+ /**
+ * Return result to precOffset[0] digits to the right of the decimal point.
+ * PrecOffset[0] is updated if the original value is out of range. No exponent or other
+ * indication of precision is added. The result is returned immediately, based on the current
+ * cache contents, but it may contain question marks for unknown digits. It may also use
+ * uncertain digits within EXTRA_DIGITS. If either of those occurred, schedule a reevaluation
+ * and redisplay operation. Uncertain digits never appear to the left of the decimal point.
+ * PrecOffset[0] may be negative to only retrieve digits to the left of the decimal point.
+ * (precOffset[0] = 0 means we include the decimal point, but nothing to the right.
+ * precOffset[0] = -1 means we drop the decimal point and start at the ones position. Should
+ * not be invoked before the onEvaluate() callback is received. This essentially just returns
+ * a substring of the full result; a leading minus sign or leading digits can be dropped.
+ * Result uses US conventions; is NOT internationalized. Use getRational() to determine
+ * whether the result is exact, or whether we dropped trailing digits.
+ *
+ * @param precOffset Zeroth element indicates desired and actual precision
+ * @param maxPrecOffset Maximum adjusted precOffset[0]
+ * @param maxDigs Maximum length of result
+ * @param truncated Zeroth element is set if leading nonzero digits were dropped
+ * @param negative Zeroth element is set of the result is negative.
+ */
+ public String getString(int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated,
+ boolean[] negative) {
+ int currentPrecOffset = precOffset[0];
// Make sure we eventually get a complete answer
- if (mCache == null) {
- ensureCachePrec(digs + EXTRA_DIGITS);
- // Nothing else to do now; seems to happen on rare occasion
- // with weird user input timing;
- // Will repair itself in a jiffy.
- return getPadding(1);
- } else {
- ensureCachePrec(digs + EXTRA_DIGITS
- + mCache.length() / EXTRA_DIVISOR);
- }
- // Compute an appropriate substring of mCache.
- // We avoid returning a huge string to minimize string
- // allocation during scrolling.
- // Pad as needed.
- final int len = mCache.length();
- final boolean myNegative = mCache.charAt(0) == '-';
- negative[0] = myNegative;
- // Don't scroll left past leftmost digits in mCache
- // unless that still leaves an integer.
- int integralDigits = len - mCacheDigs;
- // includes 1 for dec. pt
- if (myNegative) --integralDigits;
- int minDigs = Math.min(-integralDigits + MIN_DIGS, -1);
- digs = Math.min(Math.max(digs, minDigs), maxPrec);
- prec[0] = digs;
- int offset = mCacheDigs - digs; // trailing digits to drop
- int deficit = 0; // The number of digits we're short
- if (offset < 0) {
- offset = 0;
- deficit = Math.min(digs - mCacheDigs, maxDigs);
- }
- int endIndx = len - offset;
- if (endIndx < 1) return " ";
- int startIndx = (endIndx + deficit <= maxDigs) ?
- 0
- : endIndx + deficit - maxDigs;
- truncated[0] = (startIndx > getMsd());
- String res = mCache.substring(startIndx, endIndx);
- if (deficit > 0) {
- res = res + getPadding(deficit);
- // Since we always compute past the decimal point,
- // this never fills in the spot where the decimal point
- // should go, and the rest of this can treat the
- // made-up symbols as though they were digits.
+ if (mResultString == null) {
+ ensureCachePrec(currentPrecOffset + EXTRA_DIGITS);
+ // Nothing else to do now; seems to happen on rare occasion with weird user input
+ // timing; Will repair itself in a jiffy.
+ return " ";
+ } else {
+ ensureCachePrec(currentPrecOffset + EXTRA_DIGITS + mResultString.length()
+ / EXTRA_DIVISOR);
+ }
+ // Compute an appropriate substring of mResultString. Pad if necessary.
+ final int len = mResultString.length();
+ final boolean myNegative = mResultString.charAt(0) == '-';
+ negative[0] = myNegative;
+ // Don't scroll left past leftmost digits in mResultString unless that still leaves an
+ // integer.
+ int integralDigits = len - mResultStringOffset;
+ // includes 1 for dec. pt
+ if (myNegative) {
+ --integralDigits;
}
- return res;
+ int minPrecOffset = Math.min(MIN_DISPLAYED_DIGS - integralDigits, -1);
+ currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset),
+ maxPrecOffset);
+ precOffset[0] = currentPrecOffset;
+ int extraDigs = mResultStringOffset - currentPrecOffset; // trailing digits to drop
+ int deficit = 0; // The number of digits we're short
+ if (extraDigs < 0) {
+ extraDigs = 0;
+ deficit = Math.min(currentPrecOffset - mResultStringOffset, maxDigs);
+ }
+ int endIndex = len - extraDigs;
+ if (endIndex < 1) {
+ return " ";
+ }
+ int startIndex = Math.max(endIndex + deficit - maxDigs, 0);
+ truncated[0] = (startIndex > getMsdIndex());
+ String result = mResultString.substring(startIndex, endIndex);
+ if (deficit > 0) {
+ result += repeat(' ', deficit);
+ // Blank character is replaced during translation.
+ // Since we always compute past the decimal point, this never fills in the spot
+ // where the decimal point should go, and we can otherwise treat placeholders
+ // as though they were digits.
+ }
+ return result;
}
- // Return rational representation of current result, if any.
+ /**
+ * Return rational representation of current result, if any.
+ * Return null if the result is irrational, or we couldn't track the rational value,
+ * e.g. because the denominator got too big.
+ */
public BoundedRational getRational() {
return mRatVal;
}
private void clearCache() {
- mCache = null;
- mCacheDigs = mCacheDigsReq = 0;
- mMsd = INVALID_MSD;
+ mResultString = null;
+ mResultStringOffset = mResultStringOffsetReq = 0;
+ mMsdIndex = INVALID_MSD;
}
- void clear() {
+
+ private void clearPreservingTimeout() {
mExpr.clear();
clearCache();
}
+ public void clear() {
+ clearPreservingTimeout();
+ mLongTimeout = false;
+ }
+
/**
* Start asynchronous result evaluation of formula.
* Will result in display on completion.
* @param required result was explicitly requested by user.
*/
- private void reevaluateResult(boolean required) {
+ private void evaluateResult(boolean required) {
clearCache();
- mEvaluator = new AsyncDisplayResult(mDegreeMode, required);
+ mEvaluator = new AsyncEvaluator(mDegreeMode, required);
mEvaluator.execute();
mChangedValue = false;
}
- // Begin evaluation of result and display when ready.
- // We assume this is called after each insertion and deletion.
- // Thus if we are called twice with the same effective end of
- // the formula, the evaluation is redundant.
- void evaluateAndShowResult() {
+ /**
+ * Start optional evaluation of result and display when ready.
+ * Can quietly time out without a user-visible display.
+ */
+ public void evaluateAndShowResult() {
if (!mChangedValue) {
// Already done or in progress.
return;
@@ -832,25 +878,27 @@ class Evaluator {
// In very odd cases, there can be significant latency to evaluate.
// Don't show obsolete result.
mResult.clear();
- reevaluateResult(false);
+ evaluateResult(false);
}
- // Ensure that we either display a result or complain.
- // Does not invalidate a previously computed cache.
- // We presume that any prior result was computed using the same
- // expression.
- void requireResult() {
- if (mCache == null || mChangedValue) {
+ /**
+ * Start required evaluation of result and display when ready.
+ * Will eventually call back mCalculator to display result or error, or display
+ * a timeout message. Uses longer timeouts than optional evaluation.
+ */
+ public void requireResult() {
+ if (mResultString == null || mChangedValue) {
// Restart evaluator in requested mode, i.e. with longer timeout.
cancelAll(true);
- reevaluateResult(true);
+ evaluateResult(true);
} else {
// Notify immediately, reusing existing result.
- int dotPos = mCache.indexOf('.');
- String truncatedWholePart = mCache.substring(0, dotPos);
- int leastDigOffset = getLsd(mRatVal, mCache, dotPos);
- int msdIndex = getMsd();
- int preferredPrecOffset = getPreferredPrec(mCache, msdIndex, leastDigOffset);
+ final int dotIndex = mResultString.indexOf('.');
+ final String truncatedWholePart = mResultString.substring(0, dotIndex);
+ final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
+ final int msdIndex = getMsdIndex();
+ final int preferredPrecOffset = getPreferredPrec(mResultString, msdIndex,
+ leastDigOffset);
mCalculator.onEvaluate(preferredPrecOffset, msdIndex, leastDigOffset,
truncatedWholePart);
}
@@ -861,10 +909,10 @@ class Evaluator {
* @param quiet suppress cancellation message
* @return true if we cancelled an initial evaluation
*/
- boolean cancelAll(boolean quiet) {
+ public boolean cancelAll(boolean quiet) {
if (mCurrentReevaluator != null) {
mCurrentReevaluator.cancel(true);
- mCacheDigsReq = mCacheDigs;
+ mResultStringOffsetReq = mResultStringOffset;
// Backgound computation touches only constructive reals.
// OK not to wait.
mCurrentReevaluator = null;
@@ -888,11 +936,16 @@ class Evaluator {
return false;
}
- void restoreInstanceState(DataInput in) {
+ /**
+ * Restore the evaluator state, including the expression and any saved value.
+ */
+ public void restoreInstanceState(DataInput in) {
mChangedValue = true;
try {
CalculatorExpr.initExprInput();
mDegreeMode = in.readBoolean();
+ mLongTimeout = in.readBoolean();
+ mLongSavedTimeout = in.readBoolean();
mExpr = new CalculatorExpr(in);
mSavedName = in.readUTF();
mSaved = new CalculatorExpr(in);
@@ -901,10 +954,15 @@ class Evaluator {
}
}
- void saveInstanceState(DataOutput out) {
+ /**
+ * Save the evaluator state, including the expression and any saved value.
+ */
+ public void saveInstanceState(DataOutput out) {
try {
CalculatorExpr.initExprOutput();
out.writeBoolean(mDegreeMode);
+ out.writeBoolean(mLongTimeout);
+ out.writeBoolean(mLongSavedTimeout);
mExpr.write(out);
out.writeUTF(mSavedName);
mSaved.write(out);
@@ -913,11 +971,14 @@ class Evaluator {
}
}
- // Append a button press to the current expression.
- // Return false if we rejected the insertion due to obvious
- // syntax issues, and the expression is unchanged.
- // Return true otherwise.
- boolean append(int id) {
+
+ /**
+ * Append a button press to the current expression.
+ * @param id Button identifier for the character or operator to be added.
+ * @return false if we rejected the insertion due to obvious syntax issues, and the expression
+ * is unchanged; true otherwise
+ */
+ public boolean append(int id) {
if (id == R.id.fun_10pow) {
add10pow(); // Handled as macro expansion.
return true;
@@ -927,9 +988,12 @@ class Evaluator {
}
}
- void delete() {
+ public void delete() {
mChangedValue = true;
mExpr.delete();
+ if (mExpr.isEmpty()) {
+ mLongTimeout = false;
+ }
}
void setDegreeMode(boolean degreeMode) {
@@ -946,79 +1010,82 @@ class Evaluator {
}
/**
- * @return the {@link CalculatorExpr} representation of the current result
+ * @return the {@link CalculatorExpr} representation of the current result.
*/
- CalculatorExpr getResultExpr() {
- final int dotPos = mCache.indexOf('.');
- final int leastDigPos = getLsd(mRatVal, mCache, dotPos);
+ private CalculatorExpr getResultExpr() {
+ final int dotIndex = mResultString.indexOf('.');
+ final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
return mExpr.abbreviate(mVal, mRatVal, mDegreeMode,
- getShortString(mCache, getMsdPos(mCache), leastDigPos));
+ getShortString(mResultString, getMsdIndexOf(mResultString), leastDigOffset));
}
- // Abbreviate the current expression to a pre-evaluated
- // expression node, which will display as a short number.
- // This should not be called unless the expression was
- // previously evaluated and produced a non-error result.
- // Pre-evaluated expressions can never represent an
- // expression for which evaluation to a constructive real
- // diverges. Subsequent re-evaluation will also not diverge,
- // though it may generate errors of various kinds.
- // E.g. sqrt(-10^-1000)
- void collapse() {
+ /**
+ * Abbreviate the current expression to a pre-evaluated expression node.
+ * This should not be called unless the expression was previously evaluated and produced a
+ * non-error result. Pre-evaluated expressions can never represent an expression for which
+ * evaluation to a constructive real diverges. Subsequent re-evaluation will also not
+ * diverge, though it may generate errors of various kinds. E.g. sqrt(-10^-1000) .
+ */
+ public void collapse() {
final CalculatorExpr abbrvExpr = getResultExpr();
- clear();
+ clearPreservingTimeout();
mExpr.append(abbrvExpr);
mChangedValue = true;
}
- // Same as above, but put result in mSaved, leaving mExpr alone.
- // Return false if result is unavailable.
- boolean collapseToSaved() {
- if (mCache == null) {
+ /**
+ * Abbreviate current expression, and put result in mSaved.
+ * mExpr is left alone. Return false if result is unavailable.
+ */
+ public boolean collapseToSaved() {
+ if (mResultString == null) {
return false;
}
-
final CalculatorExpr abbrvExpr = getResultExpr();
mSaved.clear();
mSaved.append(abbrvExpr);
+ mLongSavedTimeout = mLongTimeout;
return true;
}
- Uri uriForSaved() {
+ private Uri uriForSaved() {
return new Uri.Builder().scheme("tag")
.encodedOpaquePart(mSavedName)
.build();
}
- // Collapse the current expression to mSaved and return a URI
- // describing this particular result, so that we can refer to it
- // later.
- Uri capture() {
+ /**
+ * Collapse the current expression to mSaved and return a URI describing it.
+ * describing this particular result, so that we can refer to it
+ * later.
+ */
+ public Uri capture() {
if (!collapseToSaved()) return null;
// Generate a new (entirely private) URI for this result.
// Attempt to conform to RFC4151, though it's unclear it matters.
- Date date = new Date();
- TimeZone tz = TimeZone.getDefault();
+ final TimeZone tz = TimeZone.getDefault();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
df.setTimeZone(tz);
- String isoDate = df.format(new Date());
+ final String isoDate = df.format(new Date());
mSavedName = "calculator2.android.com," + isoDate + ":"
- + (new Random().nextInt() & 0x3fffffff);
- Uri tag = uriForSaved();
- return tag;
+ + (new Random().nextInt() & 0x3fffffff);
+ return uriForSaved();
}
- boolean isLastSaved(Uri uri) {
+ public boolean isLastSaved(Uri uri) {
return uri.equals(uriForSaved());
}
- void addSaved() {
+ public void appendSaved() {
mChangedValue = true;
+ mLongTimeout |= mLongSavedTimeout;
mExpr.append(mSaved);
}
- // Add the power of 10 operator to the expression. This is treated
- // essentially as a macro expansion.
+ /**
+ * Add the power of 10 operator to the expression.
+ * This is treated essentially as a macro expansion.
+ */
private void add10pow() {
CalculatorExpr ten = new CalculatorExpr();
ten.add(R.id.digit_1);
@@ -1028,24 +1095,31 @@ class Evaluator {
mExpr.add(R.id.op_pow);
}
- // Retrieve the main expression being edited.
- // It is the callee's reponsibility to call cancelAll to cancel
- // ongoing concurrent computations before modifying the result.
- // TODO: Perhaps add functionality so we can keep this private?
- CalculatorExpr getExpr() {
+ /**
+ * Retrieve the main expression being edited.
+ * It is the callee's reponsibility to call cancelAll to cancel ongoing concurrent
+ * computations before modifying the result. The resulting expression should only
+ * be modified by the caller if either the expression value doesn't change, or in
+ * combination with another add() or delete() call that makes the value change apparent
+ * to us.
+ * TODO: Perhaps add functionality so we can keep this private?
+ */
+ public CalculatorExpr getExpr() {
return mExpr;
}
+ /**
+ * Maximum number of characters in a scientific notation exponent.
+ */
private static final int MAX_EXP_CHARS = 8;
/**
* Return the index of the character after the exponent starting at s[offset].
* Return offset if there is no exponent at that position.
- * Exponents have syntax E[-]digit* .
- * "E2" and "E-2" are valid. "E+2" and "e2" are not.
+ * Exponents have syntax E[-]digit* . "E2" and "E-2" are valid. "E+2" and "e2" are not.
* We allow any Unicode digits, and either of the commonly used minus characters.
*/
- static int exponentEnd(String s, int offset) {
+ public static int exponentEnd(String s, int offset) {
int i = offset;
int len = s.length();
if (i >= len - 1 || s.charAt(i) != 'E') {
@@ -1055,12 +1129,15 @@ class Evaluator {
if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
++i;
}
- if (i == len || i > offset + MAX_EXP_CHARS || !Character.isDigit(s.charAt(i))) {
+ if (i == len || !Character.isDigit(s.charAt(i))) {
return offset;
}
++i;
while (i < len && Character.isDigit(s.charAt(i))) {
++i;
+ if (i > offset + MAX_EXP_CHARS) {
+ return offset;
+ }
}
return i;
}
@@ -1068,10 +1145,10 @@ class Evaluator {
/**
* Add the exponent represented by s[begin..end) to the constant at the end of current
* expression.
- * The end of the current expression must be a constant.
- * Exponents have the same syntax as for exponentEnd().
+ * The end of the current expression must be a constant. Exponents have the same syntax as
+ * for exponentEnd().
*/
- void addExponent(String s, int begin, int end) {
+ public void addExponent(String s, int begin, int end) {
int sign = 1;
int exp = 0;
int i = begin + 1;