diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/calculator2/AlertDialogFragment.java | 54 | ||||
-rw-r--r-- | src/com/android/calculator2/BoundedRational.java | 352 | ||||
-rw-r--r-- | src/com/android/calculator2/Calculator.java | 56 | ||||
-rw-r--r-- | src/com/android/calculator2/CalculatorExpr.java | 783 | ||||
-rw-r--r-- | src/com/android/calculator2/CalculatorResult.java | 82 | ||||
-rw-r--r-- | src/com/android/calculator2/Evaluator.java | 1161 |
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; |