Home | History | Annotate | Download | only in calculator2
      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 
    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