1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calculator2;
18
19import android.app.AlertDialog;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.SharedPreferences;
23import android.net.Uri;
24import android.os.AsyncTask;
25import android.os.Handler;
26import android.preference.PreferenceManager;
27import android.support.annotation.VisibleForTesting;
28import android.util.Log;
29
30import com.hp.creals.CR;
31
32import java.io.DataInput;
33import java.io.DataOutput;
34import java.io.IOException;
35import java.math.BigInteger;
36import java.text.DateFormat;
37import java.text.SimpleDateFormat;
38import java.util.Date;
39import java.util.Random;
40import java.util.TimeZone;
41
42/**
43 * This implements the calculator evaluation logic.  The underlying expression is constructed and
44 * edited with append(), delete(), and clear().  An evaluation an then be started with a call to
45 * evaluateAndShowResult() or requireResult().  This starts an asynchronous computation, which
46 * requests display of the initial result, when available.  When initial evaluation is complete,
47 * it calls the calculator onEvaluate() method.  This occurs in a separate event, possibly quite a
48 * bit later.  Once a result has been computed, and before the underlying expression is modified,
49 * the getString() method may be used to produce Strings that represent approximations to various
50 * precisions.
51 *
52 * Actual expressions being evaluated are represented as {@link CalculatorExpr}s.
53 *
54 * The Evaluator owns the expression being edited and all associated state needed for evaluating
55 * it.  It provides functionality for saving and restoring this state.  However the current
56 * CalculatorExpr is exposed to the client, and may be directly accessed after cancelling any
57 * in-progress computations by invoking the cancelAll() method.
58 *
59 * When evaluation is requested, we invoke the eval() method on the CalculatorExpr from a
60 * background AsyncTask.  A subsequent getString() callback returns immediately, though it may
61 * return a result containing placeholder ' ' characters.  If we had to return palceholder
62 * characters, we start a background task, which invokes the onReevaluate() callback when it
63 * completes.  In either case, the background task computes the appropriate result digits by
64 * evaluating the constructive real (CR) returned by CalculatorExpr.eval() to the required
65 * precision.
66 *
67 * We cache the best decimal approximation we have already computed.  We compute generously to
68 * allow for some scrolling without recomputation and to minimize the chance of digits flipping
69 * from "0000" to "9999".  The best known result approximation is maintained as a string by
70 * mResultString (and in a different format by the CR representation of the result).  When we are
71 * in danger of not having digits to display in response to further scrolling, we also initiate a
72 * background computation to higher precision, as if we had generated placeholder characters.
73 *
74 * The code is designed to ensure that the error in the displayed result (excluding any
75 * placeholder characters) is always strictly less than 1 in the last displayed digit.  Typically
76 * we actually display a prefix of a result that has this property and additionally is computed to
77 * a significantly higher precision.  Thus we almost always round correctly towards zero.  (Fully
78 * correct rounding towards zero is not computable, at least given our representation.)
79 *
80 * Initial expression evaluation may time out.  This may happen in the case of domain errors such
81 * as division by zero, or for large computations.  We do not currently time out reevaluations to
82 * higher precision, since the original evaluation precluded a domain error that could result in
83 * non-termination.  (We may discover that a presumed zero result is actually slightly negative
84 * when re-evaluated; but that results in an exception, which we can handle.)  The user can abort
85 * either kind of computation.
86 *
87 * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is
88 * running at a time.
89 */
90class Evaluator {
91
92    // When naming variables and fields, "Offset" denotes a character offset in a string
93    // representing a decimal number, where the offset is relative to the decimal point.  1 =
94    // tenths position, -1 = units position.  Integer.MAX_VALUE is sometimes used for the offset
95    // of the last digit in an a nonterminating decimal expansion.  We use the suffix "Index" to
96    // denote a zero-based absolute index into such a string.
97
98    private static final String KEY_PREF_DEGREE_MODE = "degree_mode";
99
100    // The minimum number of extra digits we always try to compute to improve the chance of
101    // producing a correctly-rounded-towards-zero result.  The extra digits can be displayed to
102    // avoid generating placeholder digits, but should only be displayed briefly while computing.
103    private static final int EXTRA_DIGITS = 20;
104
105    // We adjust EXTRA_DIGITS by adding the length of the previous result divided by
106    // EXTRA_DIVISOR.  This helps hide recompute latency when long results are requested;
107    // We start the recomputation substantially before the need is likely to be visible.
108    private static final int EXTRA_DIVISOR = 5;
109
110    // In addition to insisting on extra digits (see above), we minimize reevaluation
111    // frequency by precomputing an extra PRECOMPUTE_DIGITS
112    // + <current_precision_offset>/PRECOMPUTE_DIVISOR digits, whenever we are forced to
113    // reevaluate.  The last term is dropped if prec < 0.
114    private static final int PRECOMPUTE_DIGITS = 30;
115    private static final int PRECOMPUTE_DIVISOR = 5;
116
117    // Initial evaluation precision.  Enough to guarantee that we can compute the short
118    // representation, and that we rarely have to evaluate nonzero results to MAX_MSD_PREC_OFFSET.
119    // It also helps if this is at least EXTRA_DIGITS + display width, so that we don't
120    // immediately need a second evaluation.
121    private static final int INIT_PREC = 50;
122
123    // The largest number of digits to the right of the decimal point to which we will evaluate to
124    // compute proper scientific notation for values close to zero.  Chosen to ensure that we
125    // always to better than IEEE double precision at identifying nonzeros.
126    private static final int MAX_MSD_PREC_OFFSET = 320;
127
128    // If we can replace an exponent by this many leading zeroes, we do so.  Also used in
129    // estimating exponent size for truncating short representation.
130    private static final int EXP_COST = 3;
131
132    private final Calculator mCalculator;
133    private final CalculatorResult mResult;
134
135    // The current caluclator expression.
136    private CalculatorExpr mExpr;
137
138    // Last saved expression.  Either null or contains a single CalculatorExpr.PreEval node.
139    private CalculatorExpr mSaved;
140
141    //  A hopefully unique name associated with mSaved.
142    private String mSavedName;
143
144    // The expression may have changed since the last evaluation in ways that would affect its
145    // value.
146    private boolean mChangedValue;
147
148    private SharedPreferences mSharedPrefs;
149
150    private boolean mDegreeMode;       // Currently in degree (not radian) mode.
151
152    private final Handler mTimeoutHandler;  // Used to schedule evaluation timeouts.
153
154    // The following are valid only if an evaluation completed successfully.
155        private CR mVal;               // Value of mExpr as constructive real.
156        private BoundedRational mRatVal; // Value of mExpr as rational or null.
157
158    // We cache the best known decimal result in mResultString.  Whenever that is
159    // non-null, it is computed to exactly mResultStringOffset, which is always > 0.
160    // The cache is filled in by the UI thread.
161    // Valid only if mResultString is non-null and !mChangedValue.
162    private String mResultString;
163    private int mResultStringOffset = 0;
164
165    // Number of digits to which (possibly incomplete) evaluation has been requested.
166    // Only accessed by UI thread.
167    private int mResultStringOffsetReq;  // Number of digits that have been
168
169    public static final int INVALID_MSD = Integer.MAX_VALUE;
170
171    // Position of most significant digit in current cached result, if determined.  This is just
172    // the index in mResultString holding the msd.
173    private int mMsdIndex = INVALID_MSD;
174
175    // Currently running expression evaluator, if any.
176    private AsyncEvaluator mEvaluator;
177
178    // The one and only un-cancelled and currently running reevaluator. Touched only by UI thread.
179    private AsyncReevaluator mCurrentReevaluator;
180
181    Evaluator(Calculator calculator,
182              CalculatorResult resultDisplay) {
183        mCalculator = calculator;
184        mResult = resultDisplay;
185        mExpr = new CalculatorExpr();
186        mSaved = new CalculatorExpr();
187        mSavedName = "none";
188        mTimeoutHandler = new Handler();
189
190        mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(calculator);
191        mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
192    }
193
194    /**
195     * Result of initial asynchronous result computation.
196     * Represents either an error or a result computed to an initial evaluation precision.
197     */
198    private static class InitialResult {
199        public final int errorResourceId;    // Error string or INVALID_RES_ID.
200        public final CR val;                 // Constructive real value.
201        public final BoundedRational ratVal; // Rational value or null.
202        public final String newResultString;       // Null iff it can't be computed.
203        public final int newResultStringOffset;
204        public final int initDisplayOffset;
205        InitialResult(CR v, BoundedRational rv, String s, int p, int idp) {
206            errorResourceId = Calculator.INVALID_RES_ID;
207            val = v;
208            ratVal = rv;
209            newResultString = s;
210            newResultStringOffset = p;
211            initDisplayOffset = idp;
212        }
213        InitialResult(int errorId) {
214            errorResourceId = errorId;
215            val = CR.valueOf(0);
216            ratVal = BoundedRational.ZERO;
217            newResultString = "BAD";
218            newResultStringOffset = 0;
219            initDisplayOffset = 0;
220        }
221        boolean isError() {
222            return errorResourceId != Calculator.INVALID_RES_ID;
223        }
224    }
225
226    private void displayCancelledMessage() {
227        new AlertDialog.Builder(mCalculator)
228            .setMessage(R.string.cancelled)
229            .setPositiveButton(R.string.dismiss,
230                new DialogInterface.OnClickListener() {
231                    public void onClick(DialogInterface d, int which) { }
232                })
233            .create()
234            .show();
235    }
236
237    // Timeout handling.
238    // Expressions are evaluated with a sort timeout or a long timeout.
239    // Each implies different maxima on both computation time and bit length.
240    // We recheck bit length separetly to avoid wasting time on decimal conversions that are
241    // destined to fail.
242
243    /**
244     * Is a long timeout in effect for the main expression?
245     */
246    private boolean mLongTimeout = false;
247
248    /**
249     * Is a long timeout in effect for the saved expression?
250     */
251    private boolean mLongSavedTimeout = false;
252
253    /**
254     * Return the timeout in milliseconds.
255     * @param longTimeout a long timeout is in effect
256     */
257    private long getTimeout(boolean longTimeout) {
258        return longTimeout ? 15000 : 2000;
259        // Exceeding a few tens of seconds increases the risk of running out of memory
260        // and impacting the rest of the system.
261    }
262
263    /**
264     * Return the maximum number of bits in the result.  Longer results are assumed to time out.
265     * @param longTimeout a long timeout is in effect
266     */
267    private int getMaxResultBits(boolean longTimeout) {
268        return longTimeout ? 350000 : 120000;
269    }
270
271    /**
272     * Timeout for unrequested, speculative evaluations, in milliseconds.
273     */
274    private final long QUICK_TIMEOUT = 1000;
275
276    /**
277     * Maximum result bit length for unrequested, speculative evaluations.
278     */
279    private final int QUICK_MAX_RESULT_BITS = 50000;
280
281    private void displayTimeoutMessage() {
282        AlertDialogFragment.showMessageDialog(mCalculator, mCalculator.getString(R.string.timeout),
283                (mLongTimeout ? null : mCalculator.getString(R.string.ok_remove_timeout)));
284    }
285
286    public void setLongTimeOut() {
287        mLongTimeout = true;
288    }
289
290    /**
291     * Compute initial cache contents and result when we're good and ready.
292     * We leave the expression display up, with scrolling disabled, until this computation
293     * completes.  Can result in an error display if something goes wrong.  By default we set a
294     * timeout to catch runaway computations.
295     */
296    class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> {
297        private boolean mDm;  // degrees
298        private boolean mRequired; // Result was requested by user.
299        private boolean mQuiet;  // Suppress cancellation message.
300        private Runnable mTimeoutRunnable = null;
301        AsyncEvaluator(boolean dm, boolean required) {
302            mDm = dm;
303            mRequired = required;
304            mQuiet = !required;
305        }
306        private void handleTimeOut() {
307            boolean running = (getStatus() != AsyncTask.Status.FINISHED);
308            if (running && cancel(true)) {
309                mEvaluator = null;
310                // Replace mExpr with clone to avoid races if task
311                // still runs for a while.
312                mExpr = (CalculatorExpr)mExpr.clone();
313                if (mRequired) {
314                    suppressCancelMessage();
315                    displayTimeoutMessage();
316                }
317            }
318        }
319        private void suppressCancelMessage() {
320            mQuiet = true;
321        }
322        @Override
323        protected void onPreExecute() {
324            long timeout = mRequired ? getTimeout(mLongTimeout) : QUICK_TIMEOUT;
325            mTimeoutRunnable = new Runnable() {
326                @Override
327                public void run() {
328                    handleTimeOut();
329                }
330            };
331            mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
332        }
333        /**
334         * Is a computed result too big for decimal conversion?
335         */
336        private boolean isTooBig(CalculatorExpr.EvalResult res) {
337            int maxBits = mRequired ? getMaxResultBits(mLongTimeout) : QUICK_MAX_RESULT_BITS;
338            if (res.ratVal != null) {
339                return res.ratVal.wholeNumberBits() > maxBits;
340            } else {
341                return res.val.get_appr(maxBits).bitLength() > 2;
342            }
343        }
344        @Override
345        protected InitialResult doInBackground(Void... nothing) {
346            try {
347                CalculatorExpr.EvalResult res = mExpr.eval(mDm);
348                if (isTooBig(res)) {
349                    // Avoid starting a long uninterruptible decimal conversion.
350                    return new InitialResult(R.string.timeout);
351                }
352                int precOffset = INIT_PREC;
353                String initResult = res.val.toString(precOffset);
354                int msd = getMsdIndexOf(initResult);
355                if (BoundedRational.asBigInteger(res.ratVal) == null
356                        && msd == INVALID_MSD) {
357                    precOffset = MAX_MSD_PREC_OFFSET;
358                    initResult = res.val.toString(precOffset);
359                    msd = getMsdIndexOf(initResult);
360                }
361                final int lsdOffset = getLsdOffset(res.ratVal, initResult,
362                        initResult.indexOf('.'));
363                final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset);
364                final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS;
365                if (newPrecOffset > precOffset) {
366                    precOffset = newPrecOffset;
367                    initResult = res.val.toString(precOffset);
368                }
369                return new InitialResult(res.val, res.ratVal,
370                        initResult, precOffset, initDisplayOffset);
371            } catch (CalculatorExpr.SyntaxException e) {
372                return new InitialResult(R.string.error_syntax);
373            } catch (BoundedRational.ZeroDivisionException e) {
374                return new InitialResult(R.string.error_zero_divide);
375            } catch(ArithmeticException e) {
376                return new InitialResult(R.string.error_nan);
377            } catch(CR.PrecisionOverflowException e) {
378                // Extremely unlikely unless we're actually dividing by zero or the like.
379                return new InitialResult(R.string.error_overflow);
380            } catch(CR.AbortedException e) {
381                return new InitialResult(R.string.error_aborted);
382            }
383        }
384        @Override
385        protected void onPostExecute(InitialResult result) {
386            mEvaluator = null;
387            mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
388            if (result.isError()) {
389                if (result.errorResourceId == R.string.timeout) {
390                    if (mRequired) {
391                        displayTimeoutMessage();
392                    }
393                    mCalculator.onCancelled();
394                } else {
395                    mCalculator.onError(result.errorResourceId);
396                }
397                return;
398            }
399            mVal = result.val;
400            mRatVal = result.ratVal;
401            // TODO: If the new result ends in lots of zeroes, and we have a rational result which
402            // is greater than (in absolute value) the result string, we should subtract 1 ulp
403            // from the result string.  That will prevent a later change from zeroes to nines.  We
404            // know that the correct, rounded-toward-zero result has nines.
405            mResultString = result.newResultString;
406            mResultStringOffset = result.newResultStringOffset;
407            final int dotIndex = mResultString.indexOf('.');
408            String truncatedWholePart = mResultString.substring(0, dotIndex);
409            // Recheck display precision; it may change, since display dimensions may have been
410            // unknow the first time.  In that case the initial evaluation precision should have
411            // been conservative.
412            // TODO: Could optimize by remembering display size and checking for change.
413            int initPrecOffset = result.initDisplayOffset;
414            final int msdIndex = getMsdIndexOf(mResultString);
415            final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
416            final int newInitPrecOffset = getPreferredPrec(mResultString, msdIndex, leastDigOffset);
417            if (newInitPrecOffset < initPrecOffset) {
418                initPrecOffset = newInitPrecOffset;
419            } else {
420                // They should be equal.  But nothing horrible should happen if they're not. e.g.
421                // because CalculatorResult.MAX_WIDTH was too small.
422            }
423            mCalculator.onEvaluate(initPrecOffset, msdIndex, leastDigOffset, truncatedWholePart);
424        }
425        @Override
426        protected void onCancelled(InitialResult result) {
427            // Invoker resets mEvaluator.
428            mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
429            if (mRequired && !mQuiet) {
430                displayCancelledMessage();
431            } // Otherwise, if mRequired, timeout processing displayed message.
432            mCalculator.onCancelled();
433            // Just drop the evaluation; Leave expression displayed.
434            return;
435        }
436    }
437
438    /**
439     * Check whether a new higher precision result flips previously computed trailing 9s
440     * to zeroes.  If so, flip them back.  Return the adjusted result.
441     * Assumes newPrecOffset >= oldPrecOffset > 0.
442     * Since our results are accurate to < 1 ulp, this can only happen if the true result
443     * is less than the new result with trailing zeroes, and thus appending 9s to the
444     * old result must also be correct.  Such flips are impossible if the newly computed
445     * digits consist of anything other than zeroes.
446     * It is unclear that there are real cases in which this is necessary,
447     * but we have failed to prove there aren't such cases.
448     */
449    @VisibleForTesting
450    static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
451            int newPrecOffset) {
452        final int oldLen = oldDigs.length();
453        if (oldDigs.charAt(oldLen - 1) != '9') {
454            return newDigs;
455        }
456        final int newLen = newDigs.length();
457        final int precDiff = newPrecOffset - oldPrecOffset;
458        final int oldLastInNew = newLen - 1 - precDiff;
459        if (newDigs.charAt(oldLastInNew) != '0') {
460            return newDigs;
461        }
462        // Earlier digits could not have changed without a 0 to 9 or 9 to 0 flip at end.
463        // The former is OK.
464        if (!newDigs.substring(newLen - precDiff).equals(repeat('0', precDiff))) {
465            throw new AssertionError("New approximation invalidates old one!");
466        }
467        return oldDigs + repeat('9', precDiff);
468    }
469
470    /**
471     * Result of asynchronous reevaluation.
472     */
473    private static class ReevalResult {
474        public final String newResultString;
475        public final int newResultStringOffset;
476        ReevalResult(String s, int p) {
477            newResultString = s;
478            newResultStringOffset = p;
479        }
480    }
481
482    /**
483     * Compute new mResultString contents to prec digits to the right of the decimal point.
484     * Ensure that onReevaluate() is called after doing so.  If the evaluation fails for reasons
485     * other than a timeout, ensure that onError() is called.
486     */
487    private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
488        @Override
489        protected ReevalResult doInBackground(Integer... prec) {
490            try {
491                final int precOffset = prec[0].intValue();
492                return new ReevalResult(mVal.toString(precOffset), precOffset);
493            } catch(ArithmeticException e) {
494                return null;
495            } catch(CR.PrecisionOverflowException e) {
496                return null;
497            } catch(CR.AbortedException e) {
498                // Should only happen if the task was cancelled, in which case we don't look at
499                // the result.
500                return null;
501            }
502        }
503
504        @Override
505        protected void onPostExecute(ReevalResult result) {
506            if (result == null) {
507                // This should only be possible in the extremely rare case of encountering a
508                // domain error while reevaluating or in case of a precision overflow.  We don't
509                // know of a way to get the latter with a plausible amount of user input.
510                mCalculator.onError(R.string.error_nan);
511            } else {
512                if (result.newResultStringOffset < mResultStringOffset) {
513                    throw new AssertionError("Unexpected onPostExecute timing");
514                }
515                mResultString = unflipZeroes(mResultString, mResultStringOffset,
516                        result.newResultString, result.newResultStringOffset);
517                mResultStringOffset = result.newResultStringOffset;
518                mCalculator.onReevaluate();
519            }
520            mCurrentReevaluator = null;
521        }
522        // On cancellation we do nothing; invoker should have left no trace of us.
523    }
524
525    /**
526     * If necessary, start an evaluation to precOffset.
527     * Ensure that the display is redrawn when it completes.
528     */
529    private void ensureCachePrec(int precOffset) {
530        if (mResultString != null && mResultStringOffset >= precOffset
531                || mResultStringOffsetReq >= precOffset) return;
532        if (mCurrentReevaluator != null) {
533            // Ensure we only have one evaluation running at a time.
534            mCurrentReevaluator.cancel(true);
535            mCurrentReevaluator = null;
536        }
537        mCurrentReevaluator = new AsyncReevaluator();
538        mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
539        if (mResultString != null) {
540            mResultStringOffsetReq += mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
541        }
542        mCurrentReevaluator.execute(mResultStringOffsetReq);
543    }
544
545    /**
546     * Return the rightmost nonzero digit position, if any.
547     * @param ratVal Rational value of result or null.
548     * @param cache Current cached decimal string representation of result.
549     * @param decIndex Index of decimal point in cache.
550     * @result Position of rightmost nonzero digit relative to decimal point.
551     *         Integer.MIN_VALUE if ratVal is zero.  Integer.MAX_VALUE if there is no lsd,
552     *         or we cannot determine it.
553     */
554    int getLsdOffset(BoundedRational ratVal, String cache, int decIndex) {
555        if (ratVal != null && ratVal.signum() == 0) return Integer.MIN_VALUE;
556        int result = BoundedRational.digitsRequired(ratVal);
557        if (result == 0) {
558            int i;
559            for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { }
560            result = i;
561        }
562        return result;
563    }
564
565    // TODO: We may want to consistently specify the position of the current result
566    // window using the left-most visible digit index instead of the offset for the rightmost one.
567    // It seems likely that would simplify the logic.
568
569    /**
570     * Retrieve the preferred precision "offset" for the currently displayed result.
571     * May be called from non-UI thread.
572     * @param cache Current approximation as string.
573     * @param msd Position of most significant digit in result.  Index in cache.
574     *            Can be INVALID_MSD if we haven't found it yet.
575     * @param lastDigitOffset Position of least significant digit (1 = tenths digit)
576     *                  or Integer.MAX_VALUE.
577     */
578    private int getPreferredPrec(String cache, int msd, int lastDigitOffset) {
579        final int lineLength = mResult.getMaxChars();
580        final int wholeSize = cache.indexOf('.');
581        final int negative = cache.charAt(0) == '-' ? 1 : 0;
582        // Don't display decimal point if result is an integer.
583        if (lastDigitOffset == 0) {
584            lastDigitOffset = -1;
585        }
586        if (lastDigitOffset != Integer.MAX_VALUE) {
587            if (wholeSize <= lineLength && lastDigitOffset <= 0) {
588                // Exact integer.  Prefer to display as integer, without decimal point.
589                return -1;
590            }
591            if (lastDigitOffset >= 0
592                    && wholeSize + lastDigitOffset + 1 /* decimal pt. */ <= lineLength) {
593                // Display full exact number wo scientific notation.
594                return lastDigitOffset;
595            }
596        }
597        if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) {
598            // Display number without scientific notation.  Treat leading zero as msd.
599            msd = wholeSize - 1;
600        }
601        if (msd > wholeSize + MAX_MSD_PREC_OFFSET) {
602            // Display a probable but uncertain 0 as "0.000000000",
603            // without exponent.  That's a judgment call, but less likely
604            // to confuse naive users.  A more informative and confusing
605            // option would be to use a large negative exponent.
606            return lineLength - 2;
607        }
608        // Return position corresponding to having msd at left, effectively
609        // presuming scientific notation that preserves the left part of the
610        // result.
611        return msd - wholeSize + lineLength - negative - 1;
612    }
613
614    private static final int SHORT_TARGET_LENGTH  = 8;
615    private static final String SHORT_UNCERTAIN_ZERO = "0.00000" + KeyMaps.ELLIPSIS;
616
617    /**
618     * Get a short representation of the value represented by the string cache.
619     * We try to match the CalculatorResult code when the result is finite
620     * and small enough to suit our needs.
621     * The result is not internationalized.
622     * @param cache String approximation of value.  Assumed to be long enough
623     *              that if it doesn't contain enough significant digits, we can
624     *              reasonably abbreviate as SHORT_UNCERTAIN_ZERO.
625     * @param msdIndex Index of most significant digit in cache, or INVALID_MSD.
626     * @param lsdOffset Position of least significant digit in finite representation,
627     *            relative to decimal point, or MAX_VALUE.
628     */
629    private String getShortString(String cache, int msdIndex, int lsdOffset) {
630        // This somewhat mirrors the display formatting code, but
631        // - The constants are different, since we don't want to use the whole display.
632        // - This is an easier problem, since we don't support scrolling and the length
633        //   is a bit flexible.
634        // TODO: Think about refactoring this to remove partial redundancy with CalculatorResult.
635        final int dotIndex = cache.indexOf('.');
636        final int negative = cache.charAt(0) == '-' ? 1 : 0;
637        final String negativeSign = negative == 1 ? "-" : "";
638
639        // Ensure we don't have to worry about running off the end of cache.
640        if (msdIndex >= cache.length() - SHORT_TARGET_LENGTH) {
641            msdIndex = INVALID_MSD;
642        }
643        if (msdIndex == INVALID_MSD) {
644            if (lsdOffset < INIT_PREC) {
645                return "0";
646            } else {
647                return SHORT_UNCERTAIN_ZERO;
648            }
649        }
650        // Avoid scientific notation for small numbers of zeros.
651        // Instead stretch significant digits to include decimal point.
652        if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH
653            && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) {
654            // Whole number that fits in allotted space.
655            // CalculatorResult would not use scientific notation either.
656            lsdOffset = -1;
657        }
658        if (msdIndex > dotIndex) {
659            if (msdIndex <= dotIndex + EXP_COST + 1) {
660                // Preferred display format inthis cases is with leading zeroes, even if
661                // it doesn't fit entirely.  Replicate that here.
662                msdIndex = dotIndex - 1;
663            } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2
664                    && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) {
665                // Fraction that fits entirely in allotted space.
666                // CalculatorResult would not use scientific notation either.
667                msdIndex = dotIndex -1;
668            }
669        }
670        int exponent = dotIndex - msdIndex;
671        if (exponent > 0) {
672            // Adjust for the fact that the decimal point itself takes space.
673            exponent--;
674        }
675        if (lsdOffset != Integer.MAX_VALUE) {
676            final int lsdIndex = dotIndex + lsdOffset;
677            final int totalDigits = lsdIndex - msdIndex + negative + 1;
678            if (totalDigits <= SHORT_TARGET_LENGTH && dotIndex > msdIndex && lsdOffset >= -1) {
679                // Fits, no exponent needed.
680                return negativeSign + cache.substring(msdIndex, lsdIndex + 1);
681            }
682            if (totalDigits <= SHORT_TARGET_LENGTH - 3) {
683                return negativeSign + cache.charAt(msdIndex) + "."
684                        + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent;
685            }
686        }
687        // We need to abbreviate.
688        if (dotIndex > msdIndex && dotIndex < msdIndex + SHORT_TARGET_LENGTH - negative - 1) {
689            return negativeSign + cache.substring(msdIndex,
690                    msdIndex + SHORT_TARGET_LENGTH - negative - 1) + KeyMaps.ELLIPSIS;
691        }
692        // Need abbreviation + exponent
693        return negativeSign + cache.charAt(msdIndex) + "."
694                + cache.substring(msdIndex + 1, msdIndex + SHORT_TARGET_LENGTH - negative - 4)
695                + KeyMaps.ELLIPSIS + "E" + exponent;
696    }
697
698    /**
699     * Return the most significant digit index in the given numeric string.
700     * Return INVALID_MSD if there are not enough digits to prove the numeric value is
701     * different from zero.  As usual, we assume an error of strictly less than 1 ulp.
702     */
703    public static int getMsdIndexOf(String s) {
704        final int len = s.length();
705        int nonzeroIndex = -1;
706        for (int i = 0; i < len; ++i) {
707            char c = s.charAt(i);
708            if (c != '-' && c != '.' && c != '0') {
709                nonzeroIndex = i;
710                break;
711            }
712        }
713        if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) {
714            return nonzeroIndex;
715        } else {
716            return INVALID_MSD;
717        }
718    }
719
720    /**
721     * Return most significant digit index in the currently computed result.
722     * Returns an index in the result character array.  Return INVALID_MSD if the current result
723     * is too close to zero to determine the result.
724     * Result is almost consistent through reevaluations: It may increase by one, once.
725     */
726    private int getMsdIndex() {
727        if (mMsdIndex != INVALID_MSD) {
728            // 0.100000... can change to 0.0999999...  We may have to correct once by one digit.
729            if (mResultString.charAt(mMsdIndex) == '0') {
730                mMsdIndex++;
731            }
732            return mMsdIndex;
733        }
734        if (mRatVal != null && mRatVal.signum() == 0) {
735            return INVALID_MSD;  // None exists
736        }
737        int result = INVALID_MSD;
738        if (mResultString != null) {
739            result = getMsdIndexOf(mResultString);
740        }
741        return result;
742    }
743
744    /**
745     * Return a string with n copies of c.
746     */
747    private static String repeat(char c, int n) {
748        StringBuilder result = new StringBuilder();
749        for (int i = 0; i < n; ++i) {
750            result.append(c);
751        }
752        return result.toString();
753    }
754
755    // Refuse to scroll past the point at which this many digits from the whole number
756    // part of the result are still displayed.  Avoids sily displays like 1E1.
757    private static final int MIN_DISPLAYED_DIGS = 5;
758
759    /**
760     * Return result to precOffset[0] digits to the right of the decimal point.
761     * PrecOffset[0] is updated if the original value is out of range.  No exponent or other
762     * indication of precision is added.  The result is returned immediately, based on the current
763     * cache contents, but it may contain question marks for unknown digits.  It may also use
764     * uncertain digits within EXTRA_DIGITS.  If either of those occurred, schedule a reevaluation
765     * and redisplay operation.  Uncertain digits never appear to the left of the decimal point.
766     * PrecOffset[0] may be negative to only retrieve digits to the left of the decimal point.
767     * (precOffset[0] = 0 means we include the decimal point, but nothing to the right.
768     * precOffset[0] = -1 means we drop the decimal point and start at the ones position.  Should
769     * not be invoked before the onEvaluate() callback is received.  This essentially just returns
770     * a substring of the full result; a leading minus sign or leading digits can be dropped.
771     * Result uses US conventions; is NOT internationalized.  Use getRational() to determine
772     * whether the result is exact, or whether we dropped trailing digits.
773     *
774     * @param precOffset Zeroth element indicates desired and actual precision
775     * @param maxPrecOffset Maximum adjusted precOffset[0]
776     * @param maxDigs Maximum length of result
777     * @param truncated Zeroth element is set if leading nonzero digits were dropped
778     * @param negative Zeroth element is set of the result is negative.
779     */
780    public String getString(int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated,
781            boolean[] negative) {
782        int currentPrecOffset = precOffset[0];
783        // Make sure we eventually get a complete answer
784        if (mResultString == null) {
785            ensureCachePrec(currentPrecOffset + EXTRA_DIGITS);
786            // Nothing else to do now; seems to happen on rare occasion with weird user input
787            // timing; Will repair itself in a jiffy.
788            return " ";
789        } else {
790            ensureCachePrec(currentPrecOffset + EXTRA_DIGITS + mResultString.length()
791                    / EXTRA_DIVISOR);
792        }
793        // Compute an appropriate substring of mResultString.  Pad if necessary.
794        final int len = mResultString.length();
795        final boolean myNegative = mResultString.charAt(0) == '-';
796        negative[0] = myNegative;
797        // Don't scroll left past leftmost digits in mResultString unless that still leaves an
798        // integer.
799            int integralDigits = len - mResultStringOffset;
800                            // includes 1 for dec. pt
801            if (myNegative) {
802                --integralDigits;
803            }
804            int minPrecOffset = Math.min(MIN_DISPLAYED_DIGS - integralDigits, -1);
805            currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset),
806                    maxPrecOffset);
807            precOffset[0] = currentPrecOffset;
808        int extraDigs = mResultStringOffset - currentPrecOffset; // trailing digits to drop
809        int deficit = 0;  // The number of digits we're short
810        if (extraDigs < 0) {
811            extraDigs = 0;
812            deficit = Math.min(currentPrecOffset - mResultStringOffset, maxDigs);
813        }
814        int endIndex = len - extraDigs;
815        if (endIndex < 1) {
816            return " ";
817        }
818        int startIndex = Math.max(endIndex + deficit - maxDigs, 0);
819        truncated[0] = (startIndex > getMsdIndex());
820        String result = mResultString.substring(startIndex, endIndex);
821        if (deficit > 0) {
822            result += repeat(' ', deficit);
823            // Blank character is replaced during translation.
824            // Since we always compute past the decimal point, this never fills in the spot
825            // where the decimal point should go, and we can otherwise treat placeholders
826            // as though they were digits.
827        }
828        return result;
829    }
830
831    /**
832     * Return rational representation of current result, if any.
833     * Return null if the result is irrational, or we couldn't track the rational value,
834     * e.g. because the denominator got too big.
835     */
836    public BoundedRational getRational() {
837        return mRatVal;
838    }
839
840    private void clearCache() {
841        mResultString = null;
842        mResultStringOffset = mResultStringOffsetReq = 0;
843        mMsdIndex = INVALID_MSD;
844    }
845
846
847    private void clearPreservingTimeout() {
848        mExpr.clear();
849        clearCache();
850    }
851
852    public void clear() {
853        clearPreservingTimeout();
854        mLongTimeout = false;
855    }
856
857    /**
858     * Start asynchronous result evaluation of formula.
859     * Will result in display on completion.
860     * @param required result was explicitly requested by user.
861     */
862    private void evaluateResult(boolean required) {
863        clearCache();
864        mEvaluator = new AsyncEvaluator(mDegreeMode, required);
865        mEvaluator.execute();
866        mChangedValue = false;
867    }
868
869    /**
870     * Start optional evaluation of result and display when ready.
871     * Can quietly time out without a user-visible display.
872     */
873    public void evaluateAndShowResult() {
874        if (!mChangedValue) {
875            // Already done or in progress.
876            return;
877        }
878        // In very odd cases, there can be significant latency to evaluate.
879        // Don't show obsolete result.
880        mResult.clear();
881        evaluateResult(false);
882    }
883
884    /**
885     * Start required evaluation of result and display when ready.
886     * Will eventually call back mCalculator to display result or error, or display
887     * a timeout message.  Uses longer timeouts than optional evaluation.
888     */
889    public void requireResult() {
890        if (mResultString == null || mChangedValue) {
891            // Restart evaluator in requested mode, i.e. with longer timeout.
892            cancelAll(true);
893            evaluateResult(true);
894        } else {
895            // Notify immediately, reusing existing result.
896            final int dotIndex = mResultString.indexOf('.');
897            final String truncatedWholePart = mResultString.substring(0, dotIndex);
898            final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
899            final int msdIndex = getMsdIndex();
900            final int preferredPrecOffset = getPreferredPrec(mResultString, msdIndex,
901                    leastDigOffset);
902            mCalculator.onEvaluate(preferredPrecOffset, msdIndex, leastDigOffset,
903                    truncatedWholePart);
904        }
905    }
906
907    /**
908     * Cancel all current background tasks.
909     * @param quiet suppress cancellation message
910     * @return      true if we cancelled an initial evaluation
911     */
912    public boolean cancelAll(boolean quiet) {
913        if (mCurrentReevaluator != null) {
914            mCurrentReevaluator.cancel(true);
915            mResultStringOffsetReq = mResultStringOffset;
916            // Backgound computation touches only constructive reals.
917            // OK not to wait.
918            mCurrentReevaluator = null;
919        }
920        if (mEvaluator != null) {
921            if (quiet) {
922                mEvaluator.suppressCancelMessage();
923            }
924            mEvaluator.cancel(true);
925            // There seems to be no good way to wait for cancellation
926            // to complete, and the evaluation continues to look at
927            // mExpr, which we will again modify.
928            // Give ourselves a new copy to work on instead.
929            mExpr = (CalculatorExpr)mExpr.clone();
930            // Approximation of constructive reals should be thread-safe,
931            // so we can let that continue until it notices the cancellation.
932            mEvaluator = null;
933            mChangedValue = true;    // Didn't do the expected evaluation.
934            return true;
935        }
936        return false;
937    }
938
939    /**
940     * Restore the evaluator state, including the expression and any saved value.
941     */
942    public void restoreInstanceState(DataInput in) {
943        mChangedValue = true;
944        try {
945            CalculatorExpr.initExprInput();
946            mDegreeMode = in.readBoolean();
947            mLongTimeout = in.readBoolean();
948            mLongSavedTimeout = in.readBoolean();
949            mExpr = new CalculatorExpr(in);
950            mSavedName = in.readUTF();
951            mSaved = new CalculatorExpr(in);
952        } catch (IOException e) {
953            Log.v("Calculator", "Exception while restoring:\n" + e);
954        }
955    }
956
957    /**
958     * Save the evaluator state, including the expression and any saved value.
959     */
960    public void saveInstanceState(DataOutput out) {
961        try {
962            CalculatorExpr.initExprOutput();
963            out.writeBoolean(mDegreeMode);
964            out.writeBoolean(mLongTimeout);
965            out.writeBoolean(mLongSavedTimeout);
966            mExpr.write(out);
967            out.writeUTF(mSavedName);
968            mSaved.write(out);
969        } catch (IOException e) {
970            Log.v("Calculator", "Exception while saving state:\n" + e);
971        }
972    }
973
974
975    /**
976     * Append a button press to the current expression.
977     * @param id Button identifier for the character or operator to be added.
978     * @return false if we rejected the insertion due to obvious syntax issues, and the expression
979     * is unchanged; true otherwise
980     */
981    public boolean append(int id) {
982        if (id == R.id.fun_10pow) {
983            add10pow();  // Handled as macro expansion.
984            return true;
985        } else {
986            mChangedValue = mChangedValue || !KeyMaps.isBinary(id);
987            return mExpr.add(id);
988        }
989    }
990
991    public void delete() {
992        mChangedValue = true;
993        mExpr.delete();
994        if (mExpr.isEmpty()) {
995            mLongTimeout = false;
996        }
997    }
998
999    void setDegreeMode(boolean degreeMode) {
1000        mChangedValue = true;
1001        mDegreeMode = degreeMode;
1002
1003        mSharedPrefs.edit()
1004                .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode)
1005                .apply();
1006    }
1007
1008    boolean getDegreeMode() {
1009        return mDegreeMode;
1010    }
1011
1012    /**
1013     * @return the {@link CalculatorExpr} representation of the current result.
1014     */
1015    private CalculatorExpr getResultExpr() {
1016        final int dotIndex = mResultString.indexOf('.');
1017        final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
1018        return mExpr.abbreviate(mVal, mRatVal, mDegreeMode,
1019                getShortString(mResultString, getMsdIndexOf(mResultString), leastDigOffset));
1020    }
1021
1022    /**
1023     * Abbreviate the current expression to a pre-evaluated expression node.
1024     * This should not be called unless the expression was previously evaluated and produced a
1025     * non-error result.  Pre-evaluated expressions can never represent an expression for which
1026     * evaluation to a constructive real diverges.  Subsequent re-evaluation will also not
1027     * diverge, though it may generate errors of various kinds.  E.g.  sqrt(-10^-1000) .
1028     */
1029    public void collapse() {
1030        final CalculatorExpr abbrvExpr = getResultExpr();
1031        clearPreservingTimeout();
1032        mExpr.append(abbrvExpr);
1033        mChangedValue = true;
1034    }
1035
1036    /**
1037     * Abbreviate current expression, and put result in mSaved.
1038     * mExpr is left alone.  Return false if result is unavailable.
1039     */
1040    public boolean collapseToSaved() {
1041        if (mResultString == null) {
1042            return false;
1043        }
1044        final CalculatorExpr abbrvExpr = getResultExpr();
1045        mSaved.clear();
1046        mSaved.append(abbrvExpr);
1047        mLongSavedTimeout = mLongTimeout;
1048        return true;
1049    }
1050
1051    private Uri uriForSaved() {
1052        return new Uri.Builder().scheme("tag")
1053                                .encodedOpaquePart(mSavedName)
1054                                .build();
1055    }
1056
1057    /**
1058     * Collapse the current expression to mSaved and return a URI describing it.
1059     * describing this particular result, so that we can refer to it
1060     * later.
1061     */
1062    public Uri capture() {
1063        if (!collapseToSaved()) return null;
1064        // Generate a new (entirely private) URI for this result.
1065        // Attempt to conform to RFC4151, though it's unclear it matters.
1066        final TimeZone tz = TimeZone.getDefault();
1067        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
1068        df.setTimeZone(tz);
1069        final String isoDate = df.format(new Date());
1070        mSavedName = "calculator2.android.com," + isoDate + ":"
1071                + (new Random().nextInt() & 0x3fffffff);
1072        return uriForSaved();
1073    }
1074
1075    public boolean isLastSaved(Uri uri) {
1076        return uri.equals(uriForSaved());
1077    }
1078
1079    public void appendSaved() {
1080        mChangedValue = true;
1081        mLongTimeout |= mLongSavedTimeout;
1082        mExpr.append(mSaved);
1083    }
1084
1085    /**
1086     * Add the power of 10 operator to the expression.
1087     * This is treated essentially as a macro expansion.
1088     */
1089    private void add10pow() {
1090        CalculatorExpr ten = new CalculatorExpr();
1091        ten.add(R.id.digit_1);
1092        ten.add(R.id.digit_0);
1093        mChangedValue = true;  // For consistency.  Reevaluation is probably not useful.
1094        mExpr.append(ten);
1095        mExpr.add(R.id.op_pow);
1096    }
1097
1098    /**
1099     * Retrieve the main expression being edited.
1100     * It is the callee's reponsibility to call cancelAll to cancel ongoing concurrent
1101     * computations before modifying the result.  The resulting expression should only
1102     * be modified by the caller if either the expression value doesn't change, or in
1103     * combination with another add() or delete() call that makes the value change apparent
1104     * to us.
1105     * TODO: Perhaps add functionality so we can keep this private?
1106     */
1107    public CalculatorExpr getExpr() {
1108        return mExpr;
1109    }
1110
1111    /**
1112     * Maximum number of characters in a scientific notation exponent.
1113     */
1114    private static final int MAX_EXP_CHARS = 8;
1115
1116    /**
1117     * Return the index of the character after the exponent starting at s[offset].
1118     * Return offset if there is no exponent at that position.
1119     * Exponents have syntax E[-]digit* .  "E2" and "E-2" are valid.  "E+2" and "e2" are not.
1120     * We allow any Unicode digits, and either of the commonly used minus characters.
1121     */
1122    public static int exponentEnd(String s, int offset) {
1123        int i = offset;
1124        int len = s.length();
1125        if (i >= len - 1 || s.charAt(i) != 'E') {
1126            return offset;
1127        }
1128        ++i;
1129        if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
1130            ++i;
1131        }
1132        if (i == len || !Character.isDigit(s.charAt(i))) {
1133            return offset;
1134        }
1135        ++i;
1136        while (i < len && Character.isDigit(s.charAt(i))) {
1137            ++i;
1138            if (i > offset + MAX_EXP_CHARS) {
1139                return offset;
1140            }
1141        }
1142        return i;
1143    }
1144
1145    /**
1146     * Add the exponent represented by s[begin..end) to the constant at the end of current
1147     * expression.
1148     * The end of the current expression must be a constant.  Exponents have the same syntax as
1149     * for exponentEnd().
1150     */
1151    public void addExponent(String s, int begin, int end) {
1152        int sign = 1;
1153        int exp = 0;
1154        int i = begin + 1;
1155        // We do the decimal conversion ourselves to exactly match exponentEnd() conventions
1156        // and handle various kinds of digits on input.  Also avoids allocation.
1157        if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
1158            sign = -1;
1159            ++i;
1160        }
1161        for (; i < end; ++i) {
1162            exp = 10 * exp + Character.digit(s.charAt(i), 10);
1163        }
1164        mExpr.addExponent(sign * exp);
1165        mChangedValue = true;
1166    }
1167}
1168