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 
17 package com.android.calculator2;
18 
19 import android.app.AlertDialog;
20 import android.content.Context;
21 import android.content.DialogInterface;
22 import android.content.SharedPreferences;
23 import android.net.Uri;
24 import android.os.AsyncTask;
25 import android.os.Handler;
26 import android.preference.PreferenceManager;
27 import android.support.annotation.VisibleForTesting;
28 import android.util.Log;
29 
30 import com.hp.creals.CR;
31 
32 import java.io.DataInput;
33 import java.io.DataOutput;
34 import java.io.IOException;
35 import java.math.BigInteger;
36 import java.text.DateFormat;
37 import java.text.SimpleDateFormat;
38 import java.util.Date;
39 import java.util.Random;
40 import 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  */
90 class 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 
Evaluator(Calculator calculator, CalculatorResult resultDisplay)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;
InitialResult(CR v, BoundedRational rv, String s, int p, int idp)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         }
InitialResult(int errorId)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         }
isError()221         boolean isError() {
222             return errorResourceId != Calculator.INVALID_RES_ID;
223         }
224     }
225 
displayCancelledMessage()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      */
getTimeout(boolean longTimeout)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      */
getMaxResultBits(boolean longTimeout)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 
displayTimeoutMessage()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 
setLongTimeOut()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;
AsyncEvaluator(boolean dm, boolean required)301         AsyncEvaluator(boolean dm, boolean required) {
302             mDm = dm;
303             mRequired = required;
304             mQuiet = !required;
305         }
handleTimeOut()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         }
suppressCancelMessage()319         private void suppressCancelMessage() {
320             mQuiet = true;
321         }
322         @Override
onPreExecute()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          */
isTooBig(CalculatorExpr.EvalResult res)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
doInBackground(Void... nothing)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
onPostExecute(InitialResult result)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
onCancelled(InitialResult result)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
unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs, int newPrecOffset)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;
ReevalResult(String s, int p)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
doInBackground(Integer... prec)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
onPostExecute(ReevalResult result)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      */
ensureCachePrec(int precOffset)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      */
getLsdOffset(BoundedRational ratVal, String cache, int decIndex)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      */
getPreferredPrec(String cache, int msd, int lastDigitOffset)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      */
getShortString(String cache, int msdIndex, int lsdOffset)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      */
getMsdIndexOf(String s)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      */
getMsdIndex()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      */
repeat(char c, int n)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      */
getString(int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated, boolean[] negative)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      */
getRational()836     public BoundedRational getRational() {
837         return mRatVal;
838     }
839 
clearCache()840     private void clearCache() {
841         mResultString = null;
842         mResultStringOffset = mResultStringOffsetReq = 0;
843         mMsdIndex = INVALID_MSD;
844     }
845 
846 
clearPreservingTimeout()847     private void clearPreservingTimeout() {
848         mExpr.clear();
849         clearCache();
850     }
851 
clear()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      */
evaluateResult(boolean required)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      */
evaluateAndShowResult()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      */
requireResult()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      */
cancelAll(boolean quiet)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      */
restoreInstanceState(DataInput in)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      */
saveInstanceState(DataOutput out)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      */
append(int id)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 
delete()991     public void delete() {
992         mChangedValue = true;
993         mExpr.delete();
994         if (mExpr.isEmpty()) {
995             mLongTimeout = false;
996         }
997     }
998 
setDegreeMode(boolean degreeMode)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 
getDegreeMode()1008     boolean getDegreeMode() {
1009         return mDegreeMode;
1010     }
1011 
1012     /**
1013      * @return the {@link CalculatorExpr} representation of the current result.
1014      */
getResultExpr()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      */
collapse()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      */
collapseToSaved()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 
uriForSaved()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      */
capture()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 
isLastSaved(Uri uri)1075     public boolean isLastSaved(Uri uri) {
1076         return uri.equals(uriForSaved());
1077     }
1078 
appendSaved()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      */
add10pow()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      */
getExpr()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      */
exponentEnd(String s, int offset)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      */
addExponent(String s, int begin, int end)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