blob: fca757d1c5d5549167b8720faec9d11694f51e80 [file] [log] [blame]
Hans Boehm84614952014-11-25 18:46:17 -08001/*
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
Hans Boehm84614952014-11-25 18:46:17 -080017package com.android.calculator2;
18
Hans Boehm84614952014-11-25 18:46:17 -080019import android.app.AlertDialog;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -070020import android.content.Context;
Hans Boehm84614952014-11-25 18:46:17 -080021import android.content.DialogInterface;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -070022import android.content.SharedPreferences;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070023import android.net.Uri;
24import android.os.AsyncTask;
Hans Boehm84614952014-11-25 18:46:17 -080025import android.os.Handler;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -070026import android.preference.PreferenceManager;
Hans Boehm79302b02015-08-05 17:20:32 -070027import android.support.annotation.VisibleForTesting;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070028import android.util.Log;
Hans Boehm84614952014-11-25 18:46:17 -080029
30import com.hp.creals.CR;
Hans Boehm84614952014-11-25 18:46:17 -080031
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070032import java.io.DataInput;
33import java.io.DataOutput;
34import java.io.IOException;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070035import java.math.BigInteger;
Justin Klaassene2711cb2015-05-28 11:13:17 -070036import java.text.DateFormat;
37import java.text.SimpleDateFormat;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070038import java.util.Date;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070039import java.util.Random;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import java.util.TimeZone;
41
Hans Boehm3666e632015-07-27 18:33:12 -070042/**
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 */
Hans Boehm84614952014-11-25 18:46:17 -080090class Evaluator {
Justin Klaassenecc69fd2015-05-07 10:30:57 -070091
Hans Boehm3666e632015-07-27 18:33:12 -070092 // 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
Justin Klaassen8b1efdb2015-06-22 15:10:53 -070098 private static final String KEY_PREF_DEGREE_MODE = "degree_mode";
99
Hans Boehm3666e632015-07-27 18:33:12 -0700100 // 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.
Hans Boehm08e8f322015-04-21 13:18:38 -0700103 private static final int EXTRA_DIGITS = 20;
Hans Boehm84614952014-11-25 18:46:17 -0800104
Hans Boehm3666e632015-07-27 18:33:12 -0700105 // 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.
Hans Boehm50ed3202015-06-09 14:35:49 -0700121 private static final int INIT_PREC = 50;
Hans Boehm3666e632015-07-27 18:33:12 -0700122
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.
Hans Boehm50ed3202015-06-09 14:35:49 -0700130 private static final int EXP_COST = 3;
Hans Boehm84614952014-11-25 18:46:17 -0800131
Hans Boehm3666e632015-07-27 18:33:12 -0700132 private final Calculator mCalculator;
133 private final CalculatorResult mResult;
Hans Boehm84614952014-11-25 18:46:17 -0800134
Hans Boehm3666e632015-07-27 18:33:12 -0700135 // The current caluclator expression.
136 private CalculatorExpr mExpr;
Hans Boehm84614952014-11-25 18:46:17 -0800137
Hans Boehm3666e632015-07-27 18:33:12 -0700138 // 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.
Hans Boehmc023b732015-04-29 11:30:47 -0700146 private boolean mChangedValue;
Hans Boehmc023b732015-04-29 11:30:47 -0700147
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700148 private SharedPreferences mSharedPrefs;
149
Hans Boehm3666e632015-07-27 18:33:12 -0700150 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
Hans Boehm84614952014-11-25 18:46:17 -0800181 Evaluator(Calculator calculator,
182 CalculatorResult resultDisplay) {
183 mCalculator = calculator;
184 mResult = resultDisplay;
185 mExpr = new CalculatorExpr();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700186 mSaved = new CalculatorExpr();
187 mSavedName = "none";
Hans Boehm84614952014-11-25 18:46:17 -0800188 mTimeoutHandler = new Handler();
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700189
190 mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(calculator);
191 mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
Hans Boehm84614952014-11-25 18:46:17 -0800192 }
193
Hans Boehm3666e632015-07-27 18:33:12 -0700194 /**
195 * Result of initial asynchronous result computation.
196 * Represents either an error or a result computed to an initial evaluation precision.
197 */
Hans Boehm84614952014-11-25 18:46:17 -0800198 private static class InitialResult {
Hans Boehm3666e632015-07-27 18:33:12 -0700199 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;
Hans Boehm84614952014-11-25 18:46:17 -0800212 }
Hans Boehm3666e632015-07-27 18:33:12 -0700213 InitialResult(int errorId) {
214 errorResourceId = errorId;
215 val = CR.valueOf(0);
216 ratVal = BoundedRational.ZERO;
217 newResultString = "BAD";
218 newResultStringOffset = 0;
219 initDisplayOffset = 0;
Hans Boehm84614952014-11-25 18:46:17 -0800220 }
221 boolean isError() {
Hans Boehm3666e632015-07-27 18:33:12 -0700222 return errorResourceId != Calculator.INVALID_RES_ID;
Hans Boehm84614952014-11-25 18:46:17 -0800223 }
Hans Boehm84614952014-11-25 18:46:17 -0800224 }
225
226 private void displayCancelledMessage() {
Hans Boehmc023b732015-04-29 11:30:47 -0700227 new AlertDialog.Builder(mCalculator)
228 .setMessage(R.string.cancelled)
Justin Klaassencc1e8e22015-06-04 18:25:46 -0700229 .setPositiveButton(R.string.dismiss,
Hans Boehmc023b732015-04-29 11:30:47 -0700230 new DialogInterface.OnClickListener() {
231 public void onClick(DialogInterface d, int which) { }
232 })
233 .create()
234 .show();
Hans Boehm84614952014-11-25 18:46:17 -0800235 }
236
Hans Boehm3666e632015-07-27 18:33:12 -0700237 // Maximum timeout for background computations. Exceeding a few tens of seconds
238 // increases the risk of running out of memory and impacting the rest of the system.
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700239 private final long MAX_TIMEOUT = 15000;
Hans Boehm3666e632015-07-27 18:33:12 -0700240
241 // Timeout for requested evaluations, in milliseconds. This is currently not saved and
242 // restored with the state; we reset the timeout when the calculator is restarted. We'll call
243 // that a feature; others might argue it's a bug.
244 private long mTimeout = 2000;
245
246 // Timeout for unrequested, speculative evaluations, in milliseconds.
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700247 private final long QUICK_TIMEOUT = 1000;
Hans Boehm3666e632015-07-27 18:33:12 -0700248
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700249 private int mMaxResultBits = 120000; // Don't try to display a larger result.
250 private final int MAX_MAX_RESULT_BITS = 350000; // Long timeout version.
251 private final int QUICK_MAX_RESULT_BITS = 50000; // Instant result version.
Hans Boehm84614952014-11-25 18:46:17 -0800252
253 private void displayTimeoutMessage() {
Justin Klaassencc1e8e22015-06-04 18:25:46 -0700254 final AlertDialog.Builder builder = new AlertDialog.Builder(mCalculator)
255 .setMessage(R.string.timeout)
256 .setNegativeButton(R.string.dismiss, null /* listener */);
Hans Boehmc023b732015-04-29 11:30:47 -0700257 if (mTimeout != MAX_TIMEOUT) {
Justin Klaassencc1e8e22015-06-04 18:25:46 -0700258 builder.setPositiveButton(R.string.ok_remove_timeout,
259 new DialogInterface.OnClickListener() {
260 public void onClick(DialogInterface d, int which) {
261 mTimeout = MAX_TIMEOUT;
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700262 mMaxResultBits = MAX_MAX_RESULT_BITS;
Justin Klaassencc1e8e22015-06-04 18:25:46 -0700263 }
264 });
Hans Boehmc023b732015-04-29 11:30:47 -0700265 }
Justin Klaassencc1e8e22015-06-04 18:25:46 -0700266 builder.show();
Hans Boehm84614952014-11-25 18:46:17 -0800267 }
268
Hans Boehm84614952014-11-25 18:46:17 -0800269 // Compute initial cache contents and result when we're good and ready.
270 // We leave the expression display up, with scrolling
271 // disabled, until this computation completes.
272 // Can result in an error display if something goes wrong.
273 // By default we set a timeout to catch runaway computations.
Hans Boehm3666e632015-07-27 18:33:12 -0700274 class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> {
Hans Boehm84614952014-11-25 18:46:17 -0800275 private boolean mDm; // degrees
276 private boolean mRequired; // Result was requested by user.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700277 private boolean mQuiet; // Suppress cancellation message.
Hans Boehmc023b732015-04-29 11:30:47 -0700278 private Runnable mTimeoutRunnable = null;
Hans Boehm3666e632015-07-27 18:33:12 -0700279 AsyncEvaluator(boolean dm, boolean required) {
Hans Boehm84614952014-11-25 18:46:17 -0800280 mDm = dm;
281 mRequired = required;
Hans Boehmc1ea0912015-06-19 15:05:07 -0700282 mQuiet = !required;
Hans Boehm84614952014-11-25 18:46:17 -0800283 }
Hans Boehmc023b732015-04-29 11:30:47 -0700284 private void handleTimeOut() {
285 boolean running = (getStatus() != AsyncTask.Status.FINISHED);
286 if (running && cancel(true)) {
287 mEvaluator = null;
288 // Replace mExpr with clone to avoid races if task
289 // still runs for a while.
290 mExpr = (CalculatorExpr)mExpr.clone();
Hans Boehmc023b732015-04-29 11:30:47 -0700291 if (mRequired) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700292 suppressCancelMessage();
Hans Boehmc023b732015-04-29 11:30:47 -0700293 displayTimeoutMessage();
294 }
295 }
296 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700297 private void suppressCancelMessage() {
298 mQuiet = true;
299 }
Hans Boehm84614952014-11-25 18:46:17 -0800300 @Override
301 protected void onPreExecute() {
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700302 long timeout = mRequired ? mTimeout : QUICK_TIMEOUT;
303 mTimeoutRunnable = new Runnable() {
304 @Override
305 public void run() {
306 handleTimeOut();
307 }
308 };
309 mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
310 }
Hans Boehm3666e632015-07-27 18:33:12 -0700311 /**
312 * Is a computed result too big for decimal conversion?
313 */
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700314 private boolean isTooBig(CalculatorExpr.EvalResult res) {
315 int maxBits = mRequired ? mMaxResultBits : QUICK_MAX_RESULT_BITS;
Hans Boehm3666e632015-07-27 18:33:12 -0700316 if (res.ratVal != null) {
317 return res.ratVal.wholeNumberBits() > maxBits;
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700318 } else {
Hans Boehm3666e632015-07-27 18:33:12 -0700319 return res.val.get_appr(maxBits).bitLength() > 2;
Hans Boehm84614952014-11-25 18:46:17 -0800320 }
321 }
322 @Override
323 protected InitialResult doInBackground(Void... nothing) {
324 try {
Hans Boehme8553762015-06-26 17:56:52 -0700325 CalculatorExpr.EvalResult res = mExpr.eval(mDm);
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700326 if (isTooBig(res)) {
327 // Avoid starting a long uninterruptible decimal conversion.
328 return new InitialResult(R.string.timeout);
329 }
Hans Boehm3666e632015-07-27 18:33:12 -0700330 int precOffset = INIT_PREC;
331 String initResult = res.val.toString(precOffset);
332 int msd = getMsdIndexOf(initResult);
333 if (BoundedRational.asBigInteger(res.ratVal) == null
Hans Boehm682ff5e2015-03-09 14:40:25 -0700334 && msd == INVALID_MSD) {
Hans Boehm3666e632015-07-27 18:33:12 -0700335 precOffset = MAX_MSD_PREC_OFFSET;
336 initResult = res.val.toString(precOffset);
337 msd = getMsdIndexOf(initResult);
Hans Boehm84614952014-11-25 18:46:17 -0800338 }
Hans Boehm3666e632015-07-27 18:33:12 -0700339 final int lsdOffset = getLsdOffset(res.ratVal, initResult,
340 initResult.indexOf('.'));
341 final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset);
342 final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS;
343 if (newPrecOffset > precOffset) {
344 precOffset = newPrecOffset;
345 initResult = res.val.toString(precOffset);
Hans Boehm84614952014-11-25 18:46:17 -0800346 }
Hans Boehm3666e632015-07-27 18:33:12 -0700347 return new InitialResult(res.val, res.ratVal,
348 initResult, precOffset, initDisplayOffset);
Hans Boehmc023b732015-04-29 11:30:47 -0700349 } catch (CalculatorExpr.SyntaxException e) {
Hans Boehm84614952014-11-25 18:46:17 -0800350 return new InitialResult(R.string.error_syntax);
Hans Boehmfbcef702015-04-27 18:07:47 -0700351 } catch (BoundedRational.ZeroDivisionException e) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700352 return new InitialResult(R.string.error_zero_divide);
Hans Boehm84614952014-11-25 18:46:17 -0800353 } catch(ArithmeticException e) {
354 return new InitialResult(R.string.error_nan);
Hans Boehm19e93c92015-06-19 18:31:28 -0700355 } catch(CR.PrecisionOverflowException e) {
Hans Boehm3666e632015-07-27 18:33:12 -0700356 // Extremely unlikely unless we're actually dividing by zero or the like.
Hans Boehm84614952014-11-25 18:46:17 -0800357 return new InitialResult(R.string.error_overflow);
Hans Boehm19e93c92015-06-19 18:31:28 -0700358 } catch(CR.AbortedException e) {
Hans Boehm84614952014-11-25 18:46:17 -0800359 return new InitialResult(R.string.error_aborted);
360 }
361 }
362 @Override
363 protected void onPostExecute(InitialResult result) {
364 mEvaluator = null;
365 mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
366 if (result.isError()) {
Hans Boehm3666e632015-07-27 18:33:12 -0700367 if (result.errorResourceId == R.string.timeout) {
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700368 if (mRequired) {
369 displayTimeoutMessage();
370 }
371 mCalculator.onCancelled();
372 } else {
Hans Boehm3666e632015-07-27 18:33:12 -0700373 mCalculator.onError(result.errorResourceId);
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700374 }
Hans Boehm84614952014-11-25 18:46:17 -0800375 return;
376 }
Hans Boehm3666e632015-07-27 18:33:12 -0700377 mVal = result.val;
378 mRatVal = result.ratVal;
379 // TODO: If the new result ends in lots of zeroes, and we have a rational result which
380 // is greater than (in absolute value) the result string, we should subtract 1 ulp
381 // from the result string. That will prevent a later change from zeroes to nines. We
382 // know that the correct, rounded-toward-zero result has nines.
383 mResultString = result.newResultString;
384 mResultStringOffset = result.newResultStringOffset;
385 final int dotIndex = mResultString.indexOf('.');
386 String truncatedWholePart = mResultString.substring(0, dotIndex);
387 // Recheck display precision; it may change, since display dimensions may have been
388 // unknow the first time. In that case the initial evaluation precision should have
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700389 // been conservative.
Hans Boehm3666e632015-07-27 18:33:12 -0700390 // TODO: Could optimize by remembering display size and checking for change.
391 int initPrecOffset = result.initDisplayOffset;
392 final int msdIndex = getMsdIndexOf(mResultString);
393 final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
394 final int newInitPrecOffset = getPreferredPrec(mResultString, msdIndex, leastDigOffset);
395 if (newInitPrecOffset < initPrecOffset) {
396 initPrecOffset = newInitPrecOffset;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700397 } else {
Hans Boehm3666e632015-07-27 18:33:12 -0700398 // They should be equal. But nothing horrible should happen if they're not. e.g.
399 // because CalculatorResult.MAX_WIDTH was too small.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700400 }
Hans Boehm3666e632015-07-27 18:33:12 -0700401 mCalculator.onEvaluate(initPrecOffset, msdIndex, leastDigOffset, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800402 }
403 @Override
404 protected void onCancelled(InitialResult result) {
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700405 // Invoker resets mEvaluator.
406 mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700407 if (mRequired && !mQuiet) {
Hans Boehmc023b732015-04-29 11:30:47 -0700408 displayCancelledMessage();
Hans Boehm3666e632015-07-27 18:33:12 -0700409 } // Otherwise, if mRequired, timeout processing displayed message.
Hans Boehm84614952014-11-25 18:46:17 -0800410 mCalculator.onCancelled();
411 // Just drop the evaluation; Leave expression displayed.
412 return;
413 }
414 }
415
Hans Boehm3666e632015-07-27 18:33:12 -0700416 /**
Hans Boehm79302b02015-08-05 17:20:32 -0700417 * Check whether a new higher precision result flips previously computed trailing 9s
418 * to zeroes. If so, flip them back. Return the adjusted result.
419 * Assumes newPrecOffset >= oldPrecOffset > 0.
420 * Since our results are accurate to < 1 ulp, this can only happen if the true result
421 * is less than the new result with trailing zeroes, and thus appending 9s to the
422 * old result must also be correct. Such flips are impossible if the newly computed
423 * digits consist of anything other than zeroes.
424 * It is unclear that there are real cases in which this is necessary,
425 * but we have failed to prove there aren't such cases.
426 */
427 @VisibleForTesting
428 static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
429 int newPrecOffset) {
430 final int oldLen = oldDigs.length();
431 if (oldDigs.charAt(oldLen - 1) != '9') {
432 return newDigs;
433 }
434 final int newLen = newDigs.length();
435 final int precDiff = newPrecOffset - oldPrecOffset;
436 final int oldLastInNew = newLen - 1 - precDiff;
437 if (newDigs.charAt(oldLastInNew) != '0') {
438 return newDigs;
439 }
440 // Earlier digits could not have changed without a 0 to 9 or 9 to 0 flip at end.
441 // The former is OK.
442 if (!newDigs.substring(newLen - precDiff).equals(repeat('0', precDiff))) {
443 throw new AssertionError("New approximation invalidates old one!");
444 }
445 return oldDigs + repeat('9', precDiff);
446 }
447
448 /**
Hans Boehm3666e632015-07-27 18:33:12 -0700449 * Result of asynchronous reevaluation.
450 */
451 private static class ReevalResult {
452 public final String newResultString;
453 public final int newResultStringOffset;
454 ReevalResult(String s, int p) {
455 newResultString = s;
456 newResultStringOffset = p;
457 }
458 }
Hans Boehm84614952014-11-25 18:46:17 -0800459
Hans Boehm3666e632015-07-27 18:33:12 -0700460 /**
461 * Compute new mResultString contents to prec digits to the right of the decimal point.
462 * Ensure that onReevaluate() is called after doing so. If the evaluation fails for reasons
463 * other than a timeout, ensure that onError() is called.
464 */
465 private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
466 @Override
467 protected ReevalResult doInBackground(Integer... prec) {
468 try {
469 final int precOffset = prec[0].intValue();
470 return new ReevalResult(mVal.toString(precOffset), precOffset);
471 } catch(ArithmeticException e) {
472 return null;
473 } catch(CR.PrecisionOverflowException e) {
474 return null;
475 } catch(CR.AbortedException e) {
476 // Should only happen if the task was cancelled, in which case we don't look at
477 // the result.
478 return null;
479 }
480 }
Hans Boehm79302b02015-08-05 17:20:32 -0700481
Hans Boehm3666e632015-07-27 18:33:12 -0700482 @Override
483 protected void onPostExecute(ReevalResult result) {
484 if (result == null) {
485 // This should only be possible in the extremely rare case of encountering a
486 // domain error while reevaluating or in case of a precision overflow. We don't
487 // know of a way to get the latter with a plausible amount of user input.
488 mCalculator.onError(R.string.error_nan);
489 } else {
490 if (result.newResultStringOffset < mResultStringOffset) {
491 throw new AssertionError("Unexpected onPostExecute timing");
492 }
Hans Boehm79302b02015-08-05 17:20:32 -0700493 mResultString = unflipZeroes(mResultString, mResultStringOffset,
494 result.newResultString, result.newResultStringOffset);
Hans Boehm3666e632015-07-27 18:33:12 -0700495 mResultStringOffset = result.newResultStringOffset;
496 mCalculator.onReevaluate();
497 }
498 mCurrentReevaluator = null;
499 }
500 // On cancellation we do nothing; invoker should have left no trace of us.
501 }
502
503 /**
504 * If necessary, start an evaluation to precOffset.
505 * Ensure that the display is redrawn when it completes.
506 */
507 private void ensureCachePrec(int precOffset) {
508 if (mResultString != null && mResultStringOffset >= precOffset
509 || mResultStringOffsetReq >= precOffset) return;
Hans Boehm84614952014-11-25 18:46:17 -0800510 if (mCurrentReevaluator != null) {
511 // Ensure we only have one evaluation running at a time.
512 mCurrentReevaluator.cancel(true);
513 mCurrentReevaluator = null;
514 }
515 mCurrentReevaluator = new AsyncReevaluator();
Hans Boehm3666e632015-07-27 18:33:12 -0700516 mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
517 if (mResultString != null) {
518 mResultStringOffsetReq += mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
Hans Boehme4579122015-05-26 17:52:32 -0700519 }
Hans Boehm3666e632015-07-27 18:33:12 -0700520 mCurrentReevaluator.execute(mResultStringOffsetReq);
Hans Boehm84614952014-11-25 18:46:17 -0800521 }
522
Hans Boehma0e45f32015-05-30 13:20:35 -0700523 /**
524 * Return the rightmost nonzero digit position, if any.
525 * @param ratVal Rational value of result or null.
526 * @param cache Current cached decimal string representation of result.
Hans Boehm3666e632015-07-27 18:33:12 -0700527 * @param decIndex Index of decimal point in cache.
Hans Boehma0e45f32015-05-30 13:20:35 -0700528 * @result Position of rightmost nonzero digit relative to decimal point.
529 * Integer.MIN_VALUE if ratVal is zero. Integer.MAX_VALUE if there is no lsd,
530 * or we cannot determine it.
531 */
Hans Boehm3666e632015-07-27 18:33:12 -0700532 int getLsdOffset(BoundedRational ratVal, String cache, int decIndex) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700533 if (ratVal != null && ratVal.signum() == 0) return Integer.MIN_VALUE;
534 int result = BoundedRational.digitsRequired(ratVal);
535 if (result == 0) {
536 int i;
Hans Boehm3666e632015-07-27 18:33:12 -0700537 for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { }
Hans Boehma0e45f32015-05-30 13:20:35 -0700538 result = i;
539 }
540 return result;
541 }
542
Hans Boehm79302b02015-08-05 17:20:32 -0700543 // TODO: We may want to consistently specify the position of the current result
544 // window using the left-most visible digit index instead of the offset for the rightmost one.
545 // It seems likely that would simplify the logic.
546
Hans Boehma0e45f32015-05-30 13:20:35 -0700547 /**
Hans Boehm79302b02015-08-05 17:20:32 -0700548 * Retrieve the preferred precision "offset" for the currently displayed result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700549 * May be called from non-UI thread.
550 * @param cache Current approximation as string.
551 * @param msd Position of most significant digit in result. Index in cache.
552 * Can be INVALID_MSD if we haven't found it yet.
Hans Boehm3666e632015-07-27 18:33:12 -0700553 * @param lastDigitOffset Position of least significant digit (1 = tenths digit)
Hans Boehma0e45f32015-05-30 13:20:35 -0700554 * or Integer.MAX_VALUE.
555 */
Hans Boehm3666e632015-07-27 18:33:12 -0700556 private int getPreferredPrec(String cache, int msd, int lastDigitOffset) {
557 final int lineLength = mResult.getMaxChars();
558 final int wholeSize = cache.indexOf('.');
559 final int negative = cache.charAt(0) == '-' ? 1 : 0;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700560 // Don't display decimal point if result is an integer.
Hans Boehm3666e632015-07-27 18:33:12 -0700561 if (lastDigitOffset == 0) {
562 lastDigitOffset = -1;
563 }
564 if (lastDigitOffset != Integer.MAX_VALUE) {
565 if (wholeSize <= lineLength && lastDigitOffset <= 0) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700566 // Exact integer. Prefer to display as integer, without decimal point.
567 return -1;
568 }
Hans Boehm3666e632015-07-27 18:33:12 -0700569 if (lastDigitOffset >= 0
570 && wholeSize + lastDigitOffset + 1 /* decimal pt. */ <= lineLength) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700571 // Display full exact number wo scientific notation.
Hans Boehm3666e632015-07-27 18:33:12 -0700572 return lastDigitOffset;
Hans Boehma0e45f32015-05-30 13:20:35 -0700573 }
Hans Boehm84614952014-11-25 18:46:17 -0800574 }
Hans Boehm50ed3202015-06-09 14:35:49 -0700575 if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700576 // Display number without scientific notation. Treat leading zero as msd.
Hans Boehm84614952014-11-25 18:46:17 -0800577 msd = wholeSize - 1;
578 }
Hans Boehm3666e632015-07-27 18:33:12 -0700579 if (msd > wholeSize + MAX_MSD_PREC_OFFSET) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700580 // Display a probable but uncertain 0 as "0.000000000",
Hans Boehm760a9dc2015-04-20 10:27:12 -0700581 // without exponent. That's a judgment call, but less likely
582 // to confuse naive users. A more informative and confusing
583 // option would be to use a large negative exponent.
584 return lineLength - 2;
585 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700586 // Return position corresponding to having msd at left, effectively
587 // presuming scientific notation that preserves the left part of the
588 // result.
589 return msd - wholeSize + lineLength - negative - 1;
Hans Boehm84614952014-11-25 18:46:17 -0800590 }
591
Hans Boehm50ed3202015-06-09 14:35:49 -0700592 private static final int SHORT_TARGET_LENGTH = 8;
593 private static final String SHORT_UNCERTAIN_ZERO = "0.00000" + KeyMaps.ELLIPSIS;
Hans Boehm013969e2015-04-13 20:29:47 -0700594
Hans Boehm50ed3202015-06-09 14:35:49 -0700595 /**
596 * Get a short representation of the value represented by the string cache.
597 * We try to match the CalculatorResult code when the result is finite
598 * and small enough to suit our needs.
599 * The result is not internationalized.
600 * @param cache String approximation of value. Assumed to be long enough
601 * that if it doesn't contain enough significant digits, we can
602 * reasonably abbreviate as SHORT_UNCERTAIN_ZERO.
603 * @param msdIndex Index of most significant digit in cache, or INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700604 * @param lsdOffset Position of least significant digit in finite representation,
Hans Boehm50ed3202015-06-09 14:35:49 -0700605 * relative to decimal point, or MAX_VALUE.
606 */
Hans Boehm3666e632015-07-27 18:33:12 -0700607 private String getShortString(String cache, int msdIndex, int lsdOffset) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700608 // This somewhat mirrors the display formatting code, but
609 // - The constants are different, since we don't want to use the whole display.
610 // - This is an easier problem, since we don't support scrolling and the length
611 // is a bit flexible.
612 // TODO: Think about refactoring this to remove partial redundancy with CalculatorResult.
613 final int dotIndex = cache.indexOf('.');
614 final int negative = cache.charAt(0) == '-' ? 1 : 0;
615 final String negativeSign = negative == 1 ? "-" : "";
616
617 // Ensure we don't have to worry about running off the end of cache.
618 if (msdIndex >= cache.length() - SHORT_TARGET_LENGTH) {
619 msdIndex = INVALID_MSD;
620 }
621 if (msdIndex == INVALID_MSD) {
Hans Boehm3666e632015-07-27 18:33:12 -0700622 if (lsdOffset < INIT_PREC) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700623 return "0";
624 } else {
625 return SHORT_UNCERTAIN_ZERO;
Hans Boehm84614952014-11-25 18:46:17 -0800626 }
Hans Boehm84614952014-11-25 18:46:17 -0800627 }
Hans Boehm50ed3202015-06-09 14:35:49 -0700628 // Avoid scientific notation for small numbers of zeros.
629 // Instead stretch significant digits to include decimal point.
Hans Boehm3666e632015-07-27 18:33:12 -0700630 if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH
631 && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700632 // Whole number that fits in allotted space.
633 // CalculatorResult would not use scientific notation either.
Hans Boehm3666e632015-07-27 18:33:12 -0700634 lsdOffset = -1;
Hans Boehm013969e2015-04-13 20:29:47 -0700635 }
Hans Boehm50ed3202015-06-09 14:35:49 -0700636 if (msdIndex > dotIndex) {
637 if (msdIndex <= dotIndex + EXP_COST + 1) {
638 // Preferred display format inthis cases is with leading zeroes, even if
639 // it doesn't fit entirely. Replicate that here.
640 msdIndex = dotIndex - 1;
Hans Boehm3666e632015-07-27 18:33:12 -0700641 } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2
642 && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700643 // Fraction that fits entirely in allotted space.
644 // CalculatorResult would not use scientific notation either.
645 msdIndex = dotIndex -1;
646 }
647 }
648 int exponent = dotIndex - msdIndex;
649 if (exponent > 0) {
650 // Adjust for the fact that the decimal point itself takes space.
651 exponent--;
652 }
Hans Boehm3666e632015-07-27 18:33:12 -0700653 if (lsdOffset != Integer.MAX_VALUE) {
654 final int lsdIndex = dotIndex + lsdOffset;
655 final int totalDigits = lsdIndex - msdIndex + negative + 1;
656 if (totalDigits <= SHORT_TARGET_LENGTH && dotIndex > msdIndex && lsdOffset >= -1) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700657 // Fits, no exponent needed.
658 return negativeSign + cache.substring(msdIndex, lsdIndex + 1);
659 }
660 if (totalDigits <= SHORT_TARGET_LENGTH - 3) {
661 return negativeSign + cache.charAt(msdIndex) + "."
Hans Boehm0b9806f2015-06-29 16:07:15 -0700662 + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent;
Hans Boehm50ed3202015-06-09 14:35:49 -0700663 }
664 }
665 // We need to abbreviate.
666 if (dotIndex > msdIndex && dotIndex < msdIndex + SHORT_TARGET_LENGTH - negative - 1) {
667 return negativeSign + cache.substring(msdIndex,
668 msdIndex + SHORT_TARGET_LENGTH - negative - 1) + KeyMaps.ELLIPSIS;
669 }
670 // Need abbreviation + exponent
671 return negativeSign + cache.charAt(msdIndex) + "."
672 + cache.substring(msdIndex + 1, msdIndex + SHORT_TARGET_LENGTH - negative - 4)
Hans Boehm0b9806f2015-06-29 16:07:15 -0700673 + KeyMaps.ELLIPSIS + "E" + exponent;
Hans Boehm84614952014-11-25 18:46:17 -0800674 }
675
Hans Boehm3666e632015-07-27 18:33:12 -0700676 /**
677 * Return the most significant digit index in the given numeric string.
678 * Return INVALID_MSD if there are not enough digits to prove the numeric value is
679 * different from zero. As usual, we assume an error of strictly less than 1 ulp.
680 */
681 public static int getMsdIndexOf(String s) {
682 final int len = s.length();
683 int nonzeroIndex = -1;
Hans Boehm84614952014-11-25 18:46:17 -0800684 for (int i = 0; i < len; ++i) {
685 char c = s.charAt(i);
686 if (c != '-' && c != '.' && c != '0') {
Hans Boehm3666e632015-07-27 18:33:12 -0700687 nonzeroIndex = i;
Hans Boehm84614952014-11-25 18:46:17 -0800688 break;
689 }
690 }
Hans Boehm3666e632015-07-27 18:33:12 -0700691 if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) {
692 return nonzeroIndex;
Hans Boehm84614952014-11-25 18:46:17 -0800693 } else {
Hans Boehm84614952014-11-25 18:46:17 -0800694 return INVALID_MSD;
695 }
Hans Boehm84614952014-11-25 18:46:17 -0800696 }
697
Hans Boehm3666e632015-07-27 18:33:12 -0700698 /**
699 * Return most significant digit index in the currently computed result.
700 * Returns an index in the result character array. Return INVALID_MSD if the current result
701 * is too close to zero to determine the result.
Hans Boehm79302b02015-08-05 17:20:32 -0700702 * Result is almost consistent through reevaluations: It may increase by one, once.
Hans Boehm3666e632015-07-27 18:33:12 -0700703 */
704 private int getMsdIndex() {
Hans Boehm79302b02015-08-05 17:20:32 -0700705 if (mMsdIndex != INVALID_MSD) {
706 // 0.100000... can change to 0.0999999... We may have to correct once by one digit.
707 if (mResultString.charAt(mMsdIndex) == '0') {
708 mMsdIndex++;
709 }
710 return mMsdIndex;
711 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700712 if (mRatVal != null && mRatVal.signum() == 0) {
Hans Boehm84614952014-11-25 18:46:17 -0800713 return INVALID_MSD; // None exists
714 }
Hans Boehm3666e632015-07-27 18:33:12 -0700715 int result = INVALID_MSD;
716 if (mResultString != null) {
717 result = getMsdIndexOf(mResultString);
Hans Boehm84614952014-11-25 18:46:17 -0800718 }
Hans Boehm3666e632015-07-27 18:33:12 -0700719 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800720 }
721
Hans Boehm3666e632015-07-27 18:33:12 -0700722 /**
Hans Boehm79302b02015-08-05 17:20:32 -0700723 * Return a string with n copies of c.
Hans Boehm3666e632015-07-27 18:33:12 -0700724 */
Hans Boehm79302b02015-08-05 17:20:32 -0700725 private static String repeat(char c, int n) {
726 StringBuilder result = new StringBuilder();
Hans Boehm84614952014-11-25 18:46:17 -0800727 for (int i = 0; i < n; ++i) {
Hans Boehm79302b02015-08-05 17:20:32 -0700728 result.append(c);
Hans Boehm84614952014-11-25 18:46:17 -0800729 }
Hans Boehm79302b02015-08-05 17:20:32 -0700730 return result.toString();
Hans Boehm84614952014-11-25 18:46:17 -0800731 }
732
Hans Boehm3666e632015-07-27 18:33:12 -0700733 // Refuse to scroll past the point at which this many digits from the whole number
734 // part of the result are still displayed. Avoids sily displays like 1E1.
735 private static final int MIN_DISPLAYED_DIGS = 5;
736
737 /**
738 * Return result to precOffset[0] digits to the right of the decimal point.
739 * PrecOffset[0] is updated if the original value is out of range. No exponent or other
740 * indication of precision is added. The result is returned immediately, based on the current
741 * cache contents, but it may contain question marks for unknown digits. It may also use
742 * uncertain digits within EXTRA_DIGITS. If either of those occurred, schedule a reevaluation
743 * and redisplay operation. Uncertain digits never appear to the left of the decimal point.
744 * PrecOffset[0] may be negative to only retrieve digits to the left of the decimal point.
745 * (precOffset[0] = 0 means we include the decimal point, but nothing to the right.
746 * precOffset[0] = -1 means we drop the decimal point and start at the ones position. Should
747 * not be invoked before the onEvaluate() callback is received. This essentially just returns
748 * a substring of the full result; a leading minus sign or leading digits can be dropped.
749 * Result uses US conventions; is NOT internationalized. Use getRational() to determine
750 * whether the result is exact, or whether we dropped trailing digits.
751 *
752 * @param precOffset Zeroth element indicates desired and actual precision
753 * @param maxPrecOffset Maximum adjusted precOffset[0]
Hans Boehm79302b02015-08-05 17:20:32 -0700754 * @param maxDigs Maximum length of result
Hans Boehm3666e632015-07-27 18:33:12 -0700755 * @param truncated Zeroth element is set if leading nonzero digits were dropped
756 * @param negative Zeroth element is set of the result is negative.
757 */
758 public String getString(int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated,
759 boolean[] negative) {
760 int currentPrecOffset = precOffset[0];
Hans Boehm84614952014-11-25 18:46:17 -0800761 // Make sure we eventually get a complete answer
Hans Boehm3666e632015-07-27 18:33:12 -0700762 if (mResultString == null) {
763 ensureCachePrec(currentPrecOffset + EXTRA_DIGITS);
764 // Nothing else to do now; seems to happen on rare occasion with weird user input
765 // timing; Will repair itself in a jiffy.
Hans Boehm79302b02015-08-05 17:20:32 -0700766 return " ";
Hans Boehm3666e632015-07-27 18:33:12 -0700767 } else {
768 ensureCachePrec(currentPrecOffset + EXTRA_DIGITS + mResultString.length()
769 / EXTRA_DIVISOR);
770 }
771 // Compute an appropriate substring of mResultString. Pad if necessary.
772 final int len = mResultString.length();
773 final boolean myNegative = mResultString.charAt(0) == '-';
774 negative[0] = myNegative;
775 // Don't scroll left past leftmost digits in mResultString unless that still leaves an
776 // integer.
777 int integralDigits = len - mResultStringOffset;
778 // includes 1 for dec. pt
779 if (myNegative) {
780 --integralDigits;
Hans Boehm84614952014-11-25 18:46:17 -0800781 }
Hans Boehm3666e632015-07-27 18:33:12 -0700782 int minPrecOffset = Math.min(MIN_DISPLAYED_DIGS - integralDigits, -1);
783 currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset),
784 maxPrecOffset);
785 precOffset[0] = currentPrecOffset;
786 int extraDigs = mResultStringOffset - currentPrecOffset; // trailing digits to drop
787 int deficit = 0; // The number of digits we're short
788 if (extraDigs < 0) {
789 extraDigs = 0;
790 deficit = Math.min(currentPrecOffset - mResultStringOffset, maxDigs);
791 }
792 int endIndex = len - extraDigs;
793 if (endIndex < 1) {
794 return " ";
795 }
796 int startIndex = Math.max(endIndex + deficit - maxDigs, 0);
797 truncated[0] = (startIndex > getMsdIndex());
798 String result = mResultString.substring(startIndex, endIndex);
799 if (deficit > 0) {
Hans Boehm79302b02015-08-05 17:20:32 -0700800 result += repeat(' ', deficit);
801 // Blank character is replaced during translation.
Hans Boehm3666e632015-07-27 18:33:12 -0700802 // Since we always compute past the decimal point, this never fills in the spot
803 // where the decimal point should go, and we can otherwise treat placeholders
804 // as though they were digits.
805 }
806 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800807 }
808
Hans Boehm3666e632015-07-27 18:33:12 -0700809 /**
810 * Return rational representation of current result, if any.
811 * Return null if the result is irrational, or we couldn't track the rational value,
812 * e.g. because the denominator got too big.
813 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700814 public BoundedRational getRational() {
815 return mRatVal;
816 }
817
Hans Boehm84614952014-11-25 18:46:17 -0800818 private void clearCache() {
Hans Boehm3666e632015-07-27 18:33:12 -0700819 mResultString = null;
820 mResultStringOffset = mResultStringOffsetReq = 0;
821 mMsdIndex = INVALID_MSD;
Hans Boehm84614952014-11-25 18:46:17 -0800822 }
823
Hans Boehm3666e632015-07-27 18:33:12 -0700824 public void clear() {
Hans Boehm84614952014-11-25 18:46:17 -0800825 mExpr.clear();
826 clearCache();
827 }
828
Hans Boehmae807e12015-07-14 15:40:52 -0700829 /**
830 * Start asynchronous result evaluation of formula.
831 * Will result in display on completion.
832 * @param required result was explicitly requested by user.
833 */
Hans Boehm3666e632015-07-27 18:33:12 -0700834 private void evaluateResult(boolean required) {
Hans Boehmae807e12015-07-14 15:40:52 -0700835 clearCache();
Hans Boehm3666e632015-07-27 18:33:12 -0700836 mEvaluator = new AsyncEvaluator(mDegreeMode, required);
Hans Boehmae807e12015-07-14 15:40:52 -0700837 mEvaluator.execute();
838 mChangedValue = false;
839 }
840
Hans Boehm3666e632015-07-27 18:33:12 -0700841 /**
842 * Start optional evaluation of result and display when ready.
843 * Can quietly time out without a user-visible display.
844 */
845 public void evaluateAndShowResult() {
Hans Boehmc023b732015-04-29 11:30:47 -0700846 if (!mChangedValue) {
847 // Already done or in progress.
848 return;
849 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700850 // In very odd cases, there can be significant latency to evaluate.
851 // Don't show obsolete result.
852 mResult.clear();
Hans Boehm3666e632015-07-27 18:33:12 -0700853 evaluateResult(false);
Hans Boehm84614952014-11-25 18:46:17 -0800854 }
855
Hans Boehm3666e632015-07-27 18:33:12 -0700856 /**
857 * Start required evaluation of result and display when ready.
858 * Will eventually call back mCalculator to display result or error, or display
859 * a timeout message. Uses longer timeouts than optional evaluation.
Hans Boehm79302b02015-08-05 17:20:32 -0700860 */
Hans Boehm3666e632015-07-27 18:33:12 -0700861 public void requireResult() {
862 if (mResultString == null || mChangedValue) {
Hans Boehme8553762015-06-26 17:56:52 -0700863 // Restart evaluator in requested mode, i.e. with longer timeout.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700864 cancelAll(true);
Hans Boehm3666e632015-07-27 18:33:12 -0700865 evaluateResult(true);
Hans Boehm84614952014-11-25 18:46:17 -0800866 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700867 // Notify immediately, reusing existing result.
Hans Boehm3666e632015-07-27 18:33:12 -0700868 final int dotIndex = mResultString.indexOf('.');
869 final String truncatedWholePart = mResultString.substring(0, dotIndex);
870 final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
871 final int msdIndex = getMsdIndex();
872 final int preferredPrecOffset = getPreferredPrec(mResultString, msdIndex,
873 leastDigOffset);
Hans Boehm15a853d2015-06-23 18:32:02 -0700874 mCalculator.onEvaluate(preferredPrecOffset, msdIndex, leastDigOffset,
875 truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800876 }
877 }
878
Hans Boehmc1ea0912015-06-19 15:05:07 -0700879 /**
880 * Cancel all current background tasks.
881 * @param quiet suppress cancellation message
882 * @return true if we cancelled an initial evaluation
883 */
Hans Boehm3666e632015-07-27 18:33:12 -0700884 public boolean cancelAll(boolean quiet) {
Hans Boehm84614952014-11-25 18:46:17 -0800885 if (mCurrentReevaluator != null) {
Hans Boehm84614952014-11-25 18:46:17 -0800886 mCurrentReevaluator.cancel(true);
Hans Boehm3666e632015-07-27 18:33:12 -0700887 mResultStringOffsetReq = mResultStringOffset;
Hans Boehm84614952014-11-25 18:46:17 -0800888 // Backgound computation touches only constructive reals.
889 // OK not to wait.
890 mCurrentReevaluator = null;
891 }
892 if (mEvaluator != null) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700893 if (quiet) {
894 mEvaluator.suppressCancelMessage();
895 }
Hans Boehm84614952014-11-25 18:46:17 -0800896 mEvaluator.cancel(true);
897 // There seems to be no good way to wait for cancellation
898 // to complete, and the evaluation continues to look at
899 // mExpr, which we will again modify.
900 // Give ourselves a new copy to work on instead.
901 mExpr = (CalculatorExpr)mExpr.clone();
902 // Approximation of constructive reals should be thread-safe,
903 // so we can let that continue until it notices the cancellation.
904 mEvaluator = null;
Hans Boehm187d3e92015-06-09 18:04:26 -0700905 mChangedValue = true; // Didn't do the expected evaluation.
Hans Boehm84614952014-11-25 18:46:17 -0800906 return true;
907 }
908 return false;
909 }
910
Hans Boehm3666e632015-07-27 18:33:12 -0700911 /**
912 * Restore the evaluator state, including the expression and any saved value.
913 */
914 public void restoreInstanceState(DataInput in) {
Hans Boehm1176f232015-05-11 16:26:03 -0700915 mChangedValue = true;
Hans Boehm84614952014-11-25 18:46:17 -0800916 try {
917 CalculatorExpr.initExprInput();
918 mDegreeMode = in.readBoolean();
919 mExpr = new CalculatorExpr(in);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700920 mSavedName = in.readUTF();
921 mSaved = new CalculatorExpr(in);
Hans Boehm84614952014-11-25 18:46:17 -0800922 } catch (IOException e) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700923 Log.v("Calculator", "Exception while restoring:\n" + e);
Hans Boehm84614952014-11-25 18:46:17 -0800924 }
925 }
926
Hans Boehm3666e632015-07-27 18:33:12 -0700927 /**
928 * Save the evaluator state, including the expression and any saved value.
929 */
930 public void saveInstanceState(DataOutput out) {
Hans Boehm84614952014-11-25 18:46:17 -0800931 try {
932 CalculatorExpr.initExprOutput();
933 out.writeBoolean(mDegreeMode);
934 mExpr.write(out);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700935 out.writeUTF(mSavedName);
936 mSaved.write(out);
Hans Boehm84614952014-11-25 18:46:17 -0800937 } catch (IOException e) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700938 Log.v("Calculator", "Exception while saving state:\n" + e);
Hans Boehm84614952014-11-25 18:46:17 -0800939 }
940 }
941
Hans Boehm3666e632015-07-27 18:33:12 -0700942
943 /**
944 * Append a button press to the current expression.
945 * @param id Button identifier for the character or operator to be added.
946 * @return false if we rejected the insertion due to obvious syntax issues, and the expression
947 * is unchanged; true otherwise
948 */
949 public boolean append(int id) {
Hans Boehm4db31b42015-05-31 12:19:05 -0700950 if (id == R.id.fun_10pow) {
951 add10pow(); // Handled as macro expansion.
952 return true;
953 } else {
Hans Boehme8553762015-06-26 17:56:52 -0700954 mChangedValue = mChangedValue || !KeyMaps.isBinary(id);
Hans Boehm4db31b42015-05-31 12:19:05 -0700955 return mExpr.add(id);
956 }
Hans Boehm84614952014-11-25 18:46:17 -0800957 }
958
Hans Boehm3666e632015-07-27 18:33:12 -0700959 public void delete() {
Hans Boehmc023b732015-04-29 11:30:47 -0700960 mChangedValue = true;
961 mExpr.delete();
962 }
963
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700964 void setDegreeMode(boolean degreeMode) {
Hans Boehmc023b732015-04-29 11:30:47 -0700965 mChangedValue = true;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700966 mDegreeMode = degreeMode;
967
968 mSharedPrefs.edit()
969 .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode)
970 .apply();
Hans Boehm682ff5e2015-03-09 14:40:25 -0700971 }
972
Hans Boehmbfe8c222015-04-02 16:26:07 -0700973 boolean getDegreeMode() {
974 return mDegreeMode;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700975 }
976
Justin Klaassen44595162015-05-28 17:55:20 -0700977 /**
Hans Boehm3666e632015-07-27 18:33:12 -0700978 * @return the {@link CalculatorExpr} representation of the current result.
Justin Klaassen44595162015-05-28 17:55:20 -0700979 */
Hans Boehm3666e632015-07-27 18:33:12 -0700980 private CalculatorExpr getResultExpr() {
981 final int dotIndex = mResultString.indexOf('.');
982 final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex);
Hans Boehm50ed3202015-06-09 14:35:49 -0700983 return mExpr.abbreviate(mVal, mRatVal, mDegreeMode,
Hans Boehm3666e632015-07-27 18:33:12 -0700984 getShortString(mResultString, getMsdIndexOf(mResultString), leastDigOffset));
Justin Klaassen44595162015-05-28 17:55:20 -0700985 }
986
Hans Boehm3666e632015-07-27 18:33:12 -0700987 /**
988 * Abbreviate the current expression to a pre-evaluated expression node.
989 * This should not be called unless the expression was previously evaluated and produced a
990 * non-error result. Pre-evaluated expressions can never represent an expression for which
991 * evaluation to a constructive real diverges. Subsequent re-evaluation will also not
992 * diverge, though it may generate errors of various kinds. E.g. sqrt(-10^-1000) .
993 */
994 public void collapse() {
Justin Klaassen44595162015-05-28 17:55:20 -0700995 final CalculatorExpr abbrvExpr = getResultExpr();
Hans Boehm84614952014-11-25 18:46:17 -0800996 clear();
997 mExpr.append(abbrvExpr);
Hans Boehm187d3e92015-06-09 18:04:26 -0700998 mChangedValue = true;
Hans Boehm84614952014-11-25 18:46:17 -0800999 }
1000
Hans Boehm3666e632015-07-27 18:33:12 -07001001 /**
1002 * Abbreviate current expression, and put result in mSaved.
1003 * mExpr is left alone. Return false if result is unavailable.
1004 */
1005 public boolean collapseToSaved() {
1006 if (mResultString == null) {
Justin Klaassen44595162015-05-28 17:55:20 -07001007 return false;
1008 }
Justin Klaassen44595162015-05-28 17:55:20 -07001009 final CalculatorExpr abbrvExpr = getResultExpr();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001010 mSaved.clear();
1011 mSaved.append(abbrvExpr);
1012 return true;
1013 }
1014
Hans Boehm3666e632015-07-27 18:33:12 -07001015 private Uri uriForSaved() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001016 return new Uri.Builder().scheme("tag")
1017 .encodedOpaquePart(mSavedName)
1018 .build();
1019 }
1020
Hans Boehm3666e632015-07-27 18:33:12 -07001021 /**
1022 * Collapse the current expression to mSaved and return a URI describing it.
1023 * describing this particular result, so that we can refer to it
1024 * later.
1025 */
1026 public Uri capture() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001027 if (!collapseToSaved()) return null;
1028 // Generate a new (entirely private) URI for this result.
1029 // Attempt to conform to RFC4151, though it's unclear it matters.
Hans Boehm3666e632015-07-27 18:33:12 -07001030 final TimeZone tz = TimeZone.getDefault();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001031 DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
1032 df.setTimeZone(tz);
Hans Boehm3666e632015-07-27 18:33:12 -07001033 final String isoDate = df.format(new Date());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001034 mSavedName = "calculator2.android.com," + isoDate + ":"
Hans Boehm3666e632015-07-27 18:33:12 -07001035 + (new Random().nextInt() & 0x3fffffff);
1036 return uriForSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001037 }
1038
Hans Boehm3666e632015-07-27 18:33:12 -07001039 public boolean isLastSaved(Uri uri) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001040 return uri.equals(uriForSaved());
1041 }
1042
Hans Boehm3666e632015-07-27 18:33:12 -07001043 public void appendSaved() {
Hans Boehmed9e6782015-05-01 17:21:13 -07001044 mChangedValue = true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001045 mExpr.append(mSaved);
1046 }
1047
Hans Boehm3666e632015-07-27 18:33:12 -07001048 /**
1049 * Add the power of 10 operator to the expression.
1050 * This is treated essentially as a macro expansion.
1051 */
Hans Boehm4db31b42015-05-31 12:19:05 -07001052 private void add10pow() {
1053 CalculatorExpr ten = new CalculatorExpr();
1054 ten.add(R.id.digit_1);
1055 ten.add(R.id.digit_0);
1056 mChangedValue = true; // For consistency. Reevaluation is probably not useful.
1057 mExpr.append(ten);
1058 mExpr.add(R.id.op_pow);
1059 }
1060
Hans Boehm3666e632015-07-27 18:33:12 -07001061 /**
1062 * Retrieve the main expression being edited.
1063 * It is the callee's reponsibility to call cancelAll to cancel ongoing concurrent
1064 * computations before modifying the result. The resulting expression should only
1065 * be modified by the caller if either the expression value doesn't change, or in
1066 * combination with another add() or delete() call that makes the value change apparent
1067 * to us.
1068 * TODO: Perhaps add functionality so we can keep this private?
1069 */
1070 public CalculatorExpr getExpr() {
Hans Boehm84614952014-11-25 18:46:17 -08001071 return mExpr;
1072 }
1073
Hans Boehm3666e632015-07-27 18:33:12 -07001074 /**
1075 * Maximum number of characters in a scientific notation exponent.
1076 */
Hans Boehm0b9806f2015-06-29 16:07:15 -07001077 private static final int MAX_EXP_CHARS = 8;
1078
1079 /**
1080 * Return the index of the character after the exponent starting at s[offset].
1081 * Return offset if there is no exponent at that position.
Hans Boehm3666e632015-07-27 18:33:12 -07001082 * Exponents have syntax E[-]digit* . "E2" and "E-2" are valid. "E+2" and "e2" are not.
Hans Boehm0b9806f2015-06-29 16:07:15 -07001083 * We allow any Unicode digits, and either of the commonly used minus characters.
1084 */
Hans Boehm3666e632015-07-27 18:33:12 -07001085 public static int exponentEnd(String s, int offset) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001086 int i = offset;
1087 int len = s.length();
1088 if (i >= len - 1 || s.charAt(i) != 'E') {
1089 return offset;
1090 }
1091 ++i;
1092 if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
1093 ++i;
1094 }
Hans Boehm79302b02015-08-05 17:20:32 -07001095 if (i == len || !Character.isDigit(s.charAt(i))) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001096 return offset;
1097 }
1098 ++i;
1099 while (i < len && Character.isDigit(s.charAt(i))) {
1100 ++i;
Hans Boehm79302b02015-08-05 17:20:32 -07001101 if (i > offset + MAX_EXP_CHARS) {
1102 return offset;
1103 }
Hans Boehm0b9806f2015-06-29 16:07:15 -07001104 }
1105 return i;
1106 }
1107
1108 /**
1109 * Add the exponent represented by s[begin..end) to the constant at the end of current
1110 * expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001111 * The end of the current expression must be a constant. Exponents have the same syntax as
1112 * for exponentEnd().
Hans Boehm0b9806f2015-06-29 16:07:15 -07001113 */
Hans Boehm3666e632015-07-27 18:33:12 -07001114 public void addExponent(String s, int begin, int end) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001115 int sign = 1;
1116 int exp = 0;
1117 int i = begin + 1;
1118 // We do the decimal conversion ourselves to exactly match exponentEnd() conventions
1119 // and handle various kinds of digits on input. Also avoids allocation.
1120 if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
1121 sign = -1;
1122 ++i;
1123 }
1124 for (; i < end; ++i) {
1125 exp = 10 * exp + Character.digit(s.charAt(i), 10);
1126 }
1127 mExpr.addExponent(sign * exp);
1128 mChangedValue = true;
1129 }
Hans Boehm84614952014-11-25 18:46:17 -08001130}