diff options
Diffstat (limited to 'src/com/android/calculator2/Evaluator.java')
-rw-r--r-- | src/com/android/calculator2/Evaluator.java | 1042 |
1 files changed, 537 insertions, 505 deletions
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java index b779a68..e1bcaf6 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; @@ -100,102 +38,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; + + // Last saved expression. Either null or contains a single CalculatorExpr.PreEval node. + private CalculatorExpr mSaved; - private AsyncDisplayResult mEvaluator; - // Currently running expression evaluator, if any. + // 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 +190,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,21 +233,18 @@ class Evaluator { .show(); } + // Maximum timeout for background computations. Exceeding a few tens of seconds + // increases the risk of running out of memory and impacting the rest of the system. 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 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. + private long mTimeout = 2000; + + // 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. @@ -342,12 +270,12 @@ class Evaluator { // 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> { + 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; @@ -379,12 +307,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; + 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 +326,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 +363,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,47 +405,117 @@ 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; } } + /** + * 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"); + } + // FIXME: We are assuming that the most significant digit never moves to the left, + // i.e. that 0.99999 doesn't ever change to 1.00000. Informally that makes sense, + // in that we can only produce the former result after a computation showing that + // the true answer is < 1 (otherwise we would have violated our 1 ulp error + // bound), and higher precision evaluations should preserve that bound. But I + // don't know how to prove that. Indeed, it seems like this could be violated + // if one of the CR operations, before rounding, produced an error that was + // almost exactly at it's error bound of 1/2ulp. (Since we calculate ahead + // so far, we really mean "almost exactly", which makes it very difficult to + // generate a test case.) + // Instead, we should just check whether (a) all added digits are zeroes, and (b) + // any trailing 9's have been replaced. In that case, we just use the original + // result with 9's appended. This must be correct, since our 1 ulp error bound + // implies that the correct answer is between the two. This has the unfortunate + // consequence that we are introducing code that is extremely unlikely to ever be + // exercised, and thus very difficult to test. + mResultString = result.newResultString; + mResultStringOffset = result.newResultStringOffset; + mCalculator.onReevaluate(); + } + mCurrentReevaluator = null; + } + // On cancellation we do nothing; invoker should have left no trace of us. + } - // 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; + /** + * 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; @@ -528,30 +527,33 @@ class Evaluator { * @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 +578,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 +596,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 +604,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 +627,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,52 +650,63 @@ 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. + */ + private int getMsdIndex() { + // FIXME: We currently never adjust msd once computed, even if the result changes + // from 0.100000... to 0.0999999... (We know it can't change in the other direction.) + // It would be cheap to increment it if the current "most significant digit" is zero. + // And it would make it easier to reason about the code. We should do that. + if (mMsdIndex != INVALID_MSD) 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 + // FIXME: I think the following conditional is no longer needed. The initial + // background evaluation already ensures that either the msd is know, or we've + // evaluated to MAX_MSD_PREC_OFFSET. + if (result == INVALID_MSD && mEvaluator == null + && mCurrentReevaluator == null && mResultStringOffset < MAX_MSD_PREC_OFFSET) { + // We assert that mResultString is not null, since there is no // evaluator running. - ensureCachePrec(MAX_MSD_PREC); + ensureCachePrec(MAX_MSD_PREC_OFFSET); // 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. + /** + * Return a string with n placeholder characters. + */ private String getPadding(int n) { StringBuilder padding = new StringBuilder(); for (int i = 0; i < n; ++i) { @@ -702,108 +715,107 @@ class Evaluator { return padding.toString(); } - // Return the number of zero characters at the beginning of s + /** + * 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; + int result = 0; + final int len = s.length(); + for (result = 0; result < len && s.charAt(result) == '0'; ++result) {} + return result; } - 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; + // 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; + + /** + * 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 getPadding(1); + } 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 += getPadding(deficit); + // 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() { + public void clear() { mExpr.clear(); clearCache(); } @@ -813,18 +825,18 @@ class Evaluator { * 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 +844,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 +875,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,10 +902,14 @@ 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(); + // FIXME: Do we still need to restore DegreeMode here? mDegreeMode = in.readBoolean(); mExpr = new CalculatorExpr(in); mSavedName = in.readUTF(); @@ -901,7 +919,10 @@ 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); @@ -913,11 +934,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,7 +951,7 @@ class Evaluator { } } - void delete() { + public void delete() { mChangedValue = true; mExpr.delete(); } @@ -946,79 +970,80 @@ 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(); 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); 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; 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 +1053,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') { @@ -1068,10 +1100,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; |