blob: 1d164b3b5e45b719ed556def9e75d1c71a66bcd0 [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 -080017//
18// This implements the calculator evaluation logic.
19// An evaluation is started with a call to evaluateAndShowResult().
20// This starts an asynchronous computation, which requests display
21// of the initial result, when available. When initial evaluation is
22// complete, it calls the calculator onEvaluate() method.
23// This occurs in a separate event, and may happen quite a bit
24// later. Once a result has been computed, and before the underlying
25// expression is modified, the getString method may be used to produce
26// Strings that represent approximations to various precisions.
27//
28// Actual expressions being evaluated are represented as CalculatorExprs,
29// which are just slightly preprocessed sequences of keypresses.
30//
31// The Evaluator owns the expression being edited and associated
32// state needed for evaluating it. It provides functionality for
33// saving and restoring this state. However the current
34// CalculatorExpr is exposed to the client, and may be directly modified
35// after cancelling any in-progress computations by invoking the
36// cancelAll() method.
37//
38// When evaluation is requested by the user, we invoke the eval
39// method on the CalculatorExpr from a background AsyncTask.
40// A subsequent getString() callback returns immediately, though it may
41// return a result containing placeholder '?' characters.
42// In that case we start a background task, which invokes the
43// onReevaluate() callback when it completes.
44// In both cases, the background task
45// computes the appropriate result digits by evaluating
46// the constructive real (CR) returned by CalculatorExpr.eval()
47// to the required precision.
48//
49// We cache the best approximation we have already computed.
50// We compute generously to allow for
51// some scrolling without recomputation and to minimize the chance of
52// digits flipping from "0000" to "9999". The best known
53// result approximation is maintained as a string by mCache (and
54// in a different format by the CR representation of the result).
55// When we are in danger of not having digits to display in response
56// to further scrolling, we initiate a background computation to higher
57// precision. If we actually do fall behind, we display placeholder
58// characters, e.g. '?', and schedule a display update when the computation
59// completes.
60// The code is designed to ensure that the error in the displayed
61// result (excluding any '?' characters) is always strictly less than 1 in
62// the last displayed digit. Typically we actually display a prefix
63// of a result that has this property and additionally is computed to
64// a significantly higher precision. Thus we almost always round correctly
65// towards zero. (Fully correct rounding towards zero is not computable.)
Hans Boehmc023b732015-04-29 11:30:47 -070066//
67// Initial expression evaluation may time out. This may happen in the
68// case of domain errors such as division by zero, or for large computations.
69// We do not currently time out reevaluations to higher precision, since
70// the original evaluation prevcluded a domain error that could result
71// in non-termination. (We may discover that a presumed zero result is
72// actually slightly negative when re-evaluated; but that results in an
73// exception, which we can handle.) The user can abort either kind
74// of computation.
75//
76// We ensure that only one evaluation of either kind (AsyncReevaluator
77// or AsyncDisplayResult) is running at a time.
Hans Boehm84614952014-11-25 18:46:17 -080078
79package com.android.calculator2;
80
Hans Boehm84614952014-11-25 18:46:17 -080081import android.app.AlertDialog;
82import android.content.DialogInterface;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070083import android.content.Context;
84import android.content.res.Resources;
85import android.net.Uri;
86import android.os.AsyncTask;
Hans Boehm84614952014-11-25 18:46:17 -080087import android.os.Handler;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070088import android.text.TextUtils;
89import android.util.Log;
90import android.view.KeyEvent;
91import android.widget.EditText;
Hans Boehm84614952014-11-25 18:46:17 -080092
93import com.hp.creals.CR;
94import com.hp.creals.PrecisionOverflowError;
95import com.hp.creals.AbortedError;
96
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070097import java.io.DataInput;
98import java.io.DataOutput;
99import java.io.IOException;
100import java.text.DateFormat;
101import java.text.DecimalFormatSymbols;
102import java.text.SimpleDateFormat;
103import java.math.BigInteger;
104import java.util.Date;
105import java.util.HashMap;
106import java.util.Locale;
107import java.util.Map.Entry;
108import java.util.Random;
109import java.util.Set;
110import java.util.TimeZone;
111
Hans Boehm84614952014-11-25 18:46:17 -0800112class Evaluator {
113 private final Calculator mCalculator;
114 private final CalculatorResult mResult; // The result display View
115 private CalculatorExpr mExpr; // Current calculator expression
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700116 private CalculatorExpr mSaved; // Last saved expression.
117 // Either null or contains a single
118 // preevaluated node.
119 private String mSavedName; // A hopefully unique name associated
120 // with mSaved.
Hans Boehm84614952014-11-25 18:46:17 -0800121 // The following are valid only if an evaluation
122 // completed successfully.
123 private CR mVal; // value of mExpr as constructive real
Hans Boehm682ff5e2015-03-09 14:40:25 -0700124 private BoundedRational mRatVal; // value of mExpr as rational or null
Hans Boehm84614952014-11-25 18:46:17 -0800125 private int mLastDigs; // Last digit argument passed to getString()
126 // for this result, or the initial preferred
127 // precision.
128 private boolean mDegreeMode; // Currently in degree (not radian) mode
129 private final Handler mTimeoutHandler;
130
131 static final char MINUS = '\u2212';
132
133 static final BigInteger BIG_MILLION = BigInteger.valueOf(1000000);
134
135
136 private final char decimalPt =
137 DecimalFormatSymbols.getInstance().getDecimalSeparator();
138
Hans Boehm08e8f322015-04-21 13:18:38 -0700139 private static final int EXTRA_DIGITS = 20;
Hans Boehm84614952014-11-25 18:46:17 -0800140 // Extra computed digits to minimize probably we will have
141 // to change our minds about digits we already displayed.
142 // (The correct digits are technically not computable using our
143 // representation: An off by one error in the last digits
144 // can affect earlier ones, even though the display is
145 // always within one in the lsd. This is only visible
146 // for results that end in EXTRA_DIGITS 9s or 0s, but are
147 // not integers.)
148 // We do use these extra digits to display while we are
149 // computing the correct answer. Thus they may be
150 // temporarily visible.
Hans Boehm08e8f322015-04-21 13:18:38 -0700151 private static final int PRECOMPUTE_DIGITS = 20;
Hans Boehm84614952014-11-25 18:46:17 -0800152 // Extra digits computed to minimize
153 // reevaluations during scrolling.
154
155 // We cache the result as a string to accelerate scrolling.
156 // The cache is filled in by the UI thread, but this may
157 // happen asynchronously, much later than the request.
158 private String mCache; // Current best known result, which includes
159 private int mCacheDigs = 0; // mCacheDigs digits to the right of the
160 // decimal point. Always positive.
161 // mCache is valid when non-null
162 // unless the expression has been
163 // changed since the last evaluation call.
164 private int mCacheDigsReq; // Number of digits that have been
165 // requested. Only touched by UI
166 // thread.
Hans Boehm08e8f322015-04-21 13:18:38 -0700167 public static final int INVALID_MSD = Integer.MAX_VALUE;
Hans Boehm84614952014-11-25 18:46:17 -0800168 private int mMsd = INVALID_MSD; // Position of most significant digit
169 // in current cached result, if determined.
170 // This is just the index in mCache
171 // holding the msd.
Hans Boehm08e8f322015-04-21 13:18:38 -0700172 private static final int MAX_MSD_PREC = 100;
Hans Boehm84614952014-11-25 18:46:17 -0800173 // The largest number of digits to the right
174 // of the decimal point to which we will
175 // evaluate to compute proper scientific
176 // notation for values close to zero.
177
178 private AsyncReevaluator mCurrentReevaluator;
179 // The one and only un-cancelled and currently running reevaluator.
180 // Touched only by UI thread.
181
182 private AsyncDisplayResult mEvaluator;
183 // Currently running expression evaluator, if any.
184
Hans Boehmc023b732015-04-29 11:30:47 -0700185 private boolean mChangedValue;
186 // Last insertion or deletion may have changed the display value
187 // of the expression.
188 // We handle deletions very conservatively.
189
Hans Boehm84614952014-11-25 18:46:17 -0800190 Evaluator(Calculator calculator,
191 CalculatorResult resultDisplay) {
192 mCalculator = calculator;
193 mResult = resultDisplay;
194 mExpr = new CalculatorExpr();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700195 mSaved = new CalculatorExpr();
196 mSavedName = "none";
Hans Boehm84614952014-11-25 18:46:17 -0800197 mTimeoutHandler = new Handler();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700198 mDegreeMode = false; // Remain compatible with previous versions.
Hans Boehm84614952014-11-25 18:46:17 -0800199 }
200
201 // Result of asynchronous reevaluation
202 class ReevalResult {
203 ReevalResult(String s, int p) {
204 mNewCache = s;
205 mNewCacheDigs = p;
206 }
207 final String mNewCache;
208 final int mNewCacheDigs;
209 }
210
211 // Compute new cache contents accurate to prec digits to the right
212 // of the decimal point. Ensure that redisplay() is called after
213 // doing so. If the evaluation fails for reasons other than a
214 // timeout, ensure that DisplayError() is called.
215 class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
216 @Override
217 protected ReevalResult doInBackground(Integer... prec) {
218 try {
219 int eval_prec = prec[0].intValue();
220 return new ReevalResult(mVal.toString(eval_prec), eval_prec);
221 } catch(ArithmeticException e) {
222 return null;
223 } catch(PrecisionOverflowError e) {
224 return null;
225 } catch(AbortedError e) {
226 // Should only happen if the task was cancelled,
227 // in which case we don't look at the result.
228 return null;
229 }
230 }
231 @Override
232 protected void onPostExecute(ReevalResult result) {
233 if (result == null) {
234 // This should only be possible in the extremely rare
Hans Boehmc023b732015-04-29 11:30:47 -0700235 // case of encountering a domain error while reevaluating
236 // or in case of a precision overflow. We don't know of
237 // a way to get the latter with a plausible amount of
238 // user input.
Hans Boehm84614952014-11-25 18:46:17 -0800239 mCalculator.onError(R.string.error_nan);
240 } else {
241 if (result.mNewCacheDigs < mCacheDigs) {
242 throw new Error("Unexpected onPostExecute timing");
243 }
244 mCache = result.mNewCache;
245 mCacheDigs = result.mNewCacheDigs;
246 mCalculator.onReevaluate();
247 }
248 mCurrentReevaluator = null;
249 }
250 // On cancellation we do nothing; invoker should have
251 // left no trace of us.
252 }
253
254 // Result of initial asynchronous computation
255 private static class InitialResult {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700256 InitialResult(CR val, BoundedRational ratVal, String s, int p, int idp) {
Hans Boehm84614952014-11-25 18:46:17 -0800257 mErrorResourceId = Calculator.INVALID_RES_ID;
258 mVal = val;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700259 mRatVal = ratVal;
Hans Boehm84614952014-11-25 18:46:17 -0800260 mNewCache = s;
261 mNewCacheDigs = p;
262 mInitDisplayPrec = idp;
263 }
264 InitialResult(int errorResourceId) {
265 mErrorResourceId = errorResourceId;
266 mVal = CR.valueOf(0);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700267 mRatVal = BoundedRational.ZERO;
Hans Boehm84614952014-11-25 18:46:17 -0800268 mNewCache = "BAD";
269 mNewCacheDigs = 0;
270 mInitDisplayPrec = 0;
271 }
272 boolean isError() {
273 return mErrorResourceId != Calculator.INVALID_RES_ID;
274 }
275 final int mErrorResourceId;
276 final CR mVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700277 final BoundedRational mRatVal;
Hans Boehm84614952014-11-25 18:46:17 -0800278 final String mNewCache; // Null iff it can't be computed.
279 final int mNewCacheDigs;
280 final int mInitDisplayPrec;
281 }
282
283 private void displayCancelledMessage() {
Hans Boehmc023b732015-04-29 11:30:47 -0700284 new AlertDialog.Builder(mCalculator)
285 .setMessage(R.string.cancelled)
286 .setPositiveButton(android.R.string.ok,
287 new DialogInterface.OnClickListener() {
288 public void onClick(DialogInterface d, int which) { }
289 })
290 .create()
291 .show();
Hans Boehm84614952014-11-25 18:46:17 -0800292 }
293
294 private final long MAX_TIMEOUT = 60000;
295 // Milliseconds.
296 // Longer is unlikely to help unless
297 // we get more heap space.
298 private long mTimeout = 2000; // Timeout for requested evaluations,
299 // in milliseconds.
300 // This is currently not saved and restored
Hans Boehmc023b732015-04-29 11:30:47 -0700301 // with the state; we reset
302 // the timeout when the
Hans Boehm84614952014-11-25 18:46:17 -0800303 // calculator is restarted.
304 // We'll call that a feature; others
305 // might argue it's a bug.
Hans Boehmc023b732015-04-29 11:30:47 -0700306 private final long mQuickTimeout = 1500;
Hans Boehm84614952014-11-25 18:46:17 -0800307 // Timeout for unrequested, speculative
308 // evaluations, in milliseconds.
Hans Boehmc023b732015-04-29 11:30:47 -0700309 // Could be shorter with a faster asin()
310 // implementation.
Hans Boehm84614952014-11-25 18:46:17 -0800311
312 private void displayTimeoutMessage() {
Hans Boehmc023b732015-04-29 11:30:47 -0700313 AlertDialog.Builder b = new AlertDialog.Builder(mCalculator);
314 b.setMessage(R.string.timeout)
315 .setNegativeButton(android.R.string.ok,
316 new DialogInterface.OnClickListener() {
317 public void onClick(DialogInterface d, int which) { }
318 });
319 if (mTimeout != MAX_TIMEOUT) {
320 b.setPositiveButton(R.string.ok_remove_timeout,
321 new DialogInterface.OnClickListener() {
322 public void onClick(DialogInterface d, int which) {
323 mTimeout = MAX_TIMEOUT;
324 }
325 });
326 }
327 b.create().show();
Hans Boehm84614952014-11-25 18:46:17 -0800328 }
329
Hans Boehm84614952014-11-25 18:46:17 -0800330 // Compute initial cache contents and result when we're good and ready.
331 // We leave the expression display up, with scrolling
332 // disabled, until this computation completes.
333 // Can result in an error display if something goes wrong.
334 // By default we set a timeout to catch runaway computations.
335 class AsyncDisplayResult extends AsyncTask<Void, Void, InitialResult> {
336 private boolean mDm; // degrees
337 private boolean mRequired; // Result was requested by user.
Hans Boehmc023b732015-04-29 11:30:47 -0700338 private boolean mTimedOut = false;
339 private Runnable mTimeoutRunnable = null;
Hans Boehm84614952014-11-25 18:46:17 -0800340 AsyncDisplayResult(boolean dm, boolean required) {
341 mDm = dm;
342 mRequired = required;
343 }
Hans Boehmc023b732015-04-29 11:30:47 -0700344 private void handleTimeOut() {
345 boolean running = (getStatus() != AsyncTask.Status.FINISHED);
346 if (running && cancel(true)) {
347 mEvaluator = null;
348 // Replace mExpr with clone to avoid races if task
349 // still runs for a while.
350 mExpr = (CalculatorExpr)mExpr.clone();
351 mTimedOut = true;
352 if (mRequired) {
353 displayTimeoutMessage();
354 }
355 }
356 }
Hans Boehm84614952014-11-25 18:46:17 -0800357 @Override
358 protected void onPreExecute() {
Hans Boehm08e8f322015-04-21 13:18:38 -0700359 long timeout = mRequired ? mTimeout : mQuickTimeout;
Hans Boehm84614952014-11-25 18:46:17 -0800360 if (timeout != 0) {
Hans Boehmc023b732015-04-29 11:30:47 -0700361 mTimeoutRunnable = new Runnable() {
362 @Override
363 public void run() {
364 handleTimeOut();
365 }
366 };
367 mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
368 mTimedOut = false;
Hans Boehm84614952014-11-25 18:46:17 -0800369 }
370 }
371 @Override
372 protected InitialResult doInBackground(Void... nothing) {
373 try {
Hans Boehmc023b732015-04-29 11:30:47 -0700374 CalculatorExpr.EvalResult res = mExpr.eval(mDm, mRequired);
Hans Boehm84614952014-11-25 18:46:17 -0800375 int prec = 3; // Enough for short representation
376 String initCache = res.mVal.toString(prec);
377 int msd = getMsdPos(initCache);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700378 if (BoundedRational.asBigInteger(res.mRatVal) == null
379 && msd == INVALID_MSD) {
Hans Boehm84614952014-11-25 18:46:17 -0800380 prec = MAX_MSD_PREC;
381 initCache = res.mVal.toString(prec);
382 msd = getMsdPos(initCache);
383 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700384 int initDisplayPrec =
385 getPreferredPrec(initCache, msd,
386 BoundedRational.digitsRequired(res.mRatVal));
Hans Boehm84614952014-11-25 18:46:17 -0800387 int newPrec = initDisplayPrec + EXTRA_DIGITS;
388 if (newPrec > prec) {
389 prec = newPrec;
390 initCache = res.mVal.toString(prec);
391 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700392 return new InitialResult(res.mVal, res.mRatVal,
Hans Boehm84614952014-11-25 18:46:17 -0800393 initCache, prec, initDisplayPrec);
Hans Boehmc023b732015-04-29 11:30:47 -0700394 } catch (CalculatorExpr.SyntaxException e) {
Hans Boehm84614952014-11-25 18:46:17 -0800395 return new InitialResult(R.string.error_syntax);
Hans Boehmfbcef702015-04-27 18:07:47 -0700396 } catch (BoundedRational.ZeroDivisionException e) {
397 // Division by zero caught by BoundedRational;
398 // the easy and more common case.
399 return new InitialResult(R.string.error_zero_divide);
Hans Boehm84614952014-11-25 18:46:17 -0800400 } catch(ArithmeticException e) {
401 return new InitialResult(R.string.error_nan);
402 } catch(PrecisionOverflowError e) {
403 // Extremely unlikely unless we're actually dividing by
404 // zero or the like.
405 return new InitialResult(R.string.error_overflow);
406 } catch(AbortedError e) {
407 return new InitialResult(R.string.error_aborted);
408 }
409 }
410 @Override
411 protected void onPostExecute(InitialResult result) {
412 mEvaluator = null;
413 mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
414 if (result.isError()) {
415 mCalculator.onError(result.mErrorResourceId);
416 return;
417 }
418 mVal = result.mVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700419 mRatVal = result.mRatVal;
Hans Boehm84614952014-11-25 18:46:17 -0800420 mCache = result.mNewCache;
421 mCacheDigs = result.mNewCacheDigs;
422 mLastDigs = result.mInitDisplayPrec;
423 int dotPos = mCache.indexOf('.');
424 String truncatedWholePart = mCache.substring(0, dotPos);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700425 // Recheck display precision; it may change, since
426 // display dimensions may have been unknow the first time.
427 // In that case the initial evaluation precision should have
428 // been conservative.
429 // TODO: Could optimize by remembering display size and
430 // checking for change.
431 int init_prec = result.mInitDisplayPrec;
432 int msd = getMsdPos(mCache);
433 int new_init_prec = getPreferredPrec(mCache, msd,
434 BoundedRational.digitsRequired(mRatVal));
435 if (new_init_prec < init_prec) {
436 init_prec = new_init_prec;
437 } else {
438 // They should be equal. But nothing horrible should
439 // happen if they're not. e.g. because
440 // CalculatorResult.MAX_WIDTH was too small.
441 }
442 mCalculator.onEvaluate(init_prec,truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800443 }
444 @Override
445 protected void onCancelled(InitialResult result) {
Hans Boehmc023b732015-04-29 11:30:47 -0700446 if (mRequired && !mTimedOut) {
447 displayCancelledMessage();
448 } // Otherwise timeout processing displayed message.
Hans Boehm84614952014-11-25 18:46:17 -0800449 mCalculator.onCancelled();
450 // Just drop the evaluation; Leave expression displayed.
451 return;
452 }
453 }
454
455
456 // Start an evaluation to prec, and ensure that the
457 // display is redrawn when it completes.
458 private void ensureCachePrec(int prec) {
459 if (mCache != null && mCacheDigs >= prec
460 || mCacheDigsReq >= prec) return;
461 if (mCurrentReevaluator != null) {
462 // Ensure we only have one evaluation running at a time.
463 mCurrentReevaluator.cancel(true);
464 mCurrentReevaluator = null;
465 }
466 mCurrentReevaluator = new AsyncReevaluator();
467 mCacheDigsReq = prec + PRECOMPUTE_DIGITS;
468 mCurrentReevaluator.execute(mCacheDigsReq);
469 }
470
471 // Retrieve the preferred precision for the currently
472 // displayed result, given the number of characters we
473 // have room for and the current string approximation for
474 // the result.
Hans Boehm682ff5e2015-03-09 14:40:25 -0700475 // lastDigit is the position of the last digit on the right
Hans Boehm760a9dc2015-04-20 10:27:12 -0700476 // if there is such a thing, or Integer.MAX_VALUE.
Hans Boehm84614952014-11-25 18:46:17 -0800477 // May be called in non-UI thread.
Hans Boehm682ff5e2015-03-09 14:40:25 -0700478 int getPreferredPrec(String cache, int msd, int lastDigit) {
Hans Boehm84614952014-11-25 18:46:17 -0800479 int lineLength = mResult.getMaxChars();
480 int wholeSize = cache.indexOf('.');
Hans Boehm682ff5e2015-03-09 14:40:25 -0700481 // Don't display decimal point if result is an integer.
482 if (lastDigit == 0) lastDigit = -1;
483 if (lastDigit != Integer.MAX_VALUE
484 && ((wholeSize <= lineLength && lastDigit == 0)
485 || wholeSize + lastDigit + 1 /* d.p. */ <= lineLength)) {
Hans Boehm84614952014-11-25 18:46:17 -0800486 // Prefer to display as integer, without decimal point
Hans Boehm682ff5e2015-03-09 14:40:25 -0700487 if (lastDigit == 0) return -1;
488 return lastDigit;
Hans Boehm84614952014-11-25 18:46:17 -0800489 }
490 if (msd > wholeSize && msd <= wholeSize + 4) {
491 // Display number without scientific notation.
492 // Treat leading zero as msd.
493 msd = wholeSize - 1;
494 }
Hans Boehm760a9dc2015-04-20 10:27:12 -0700495 if (msd > wholeSize + MAX_MSD_PREC) {
496 // Display a probably but uncertain 0 as "0.000000000",
497 // without exponent. That's a judgment call, but less likely
498 // to confuse naive users. A more informative and confusing
499 // option would be to use a large negative exponent.
500 return lineLength - 2;
501 }
502 return msd - wholeSize + lineLength - 2;
Hans Boehm84614952014-11-25 18:46:17 -0800503 }
504
505 // Get a short representation of the value represented by
506 // the string cache (presumed to contain at least 5 characters)
507 // and possibly the exact integer i.
508 private String getShortString(String cache, BigInteger i) {
Hans Boehm013969e2015-04-13 20:29:47 -0700509 // The result is internationalized; we only display it.
510 String res;
511 boolean need_ellipsis = false;
512
Hans Boehm84614952014-11-25 18:46:17 -0800513 if (i != null && i.abs().compareTo(BIG_MILLION) < 0) {
Hans Boehm013969e2015-04-13 20:29:47 -0700514 res = i.toString();
Hans Boehm84614952014-11-25 18:46:17 -0800515 } else {
Hans Boehm013969e2015-04-13 20:29:47 -0700516 res = cache.substring(0,5);
Hans Boehm84614952014-11-25 18:46:17 -0800517 // Avoid a trailing period; doesn't work with ellipsis
518 if (res.charAt(3) != '.') {
519 res = res.substring(0,4);
520 }
Hans Boehm013969e2015-04-13 20:29:47 -0700521 // TODO: Don't do this in the unlikely case this is the
522 // full representation.
523 need_ellipsis = true;
Hans Boehm84614952014-11-25 18:46:17 -0800524 }
Hans Boehm013969e2015-04-13 20:29:47 -0700525 res = KeyMaps.translateResult(res);
526 if (need_ellipsis) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700527 res += KeyMaps.ELLIPSIS;
Hans Boehm013969e2015-04-13 20:29:47 -0700528 }
529 return res;
Hans Boehm84614952014-11-25 18:46:17 -0800530 }
531
532 // Return the most significant digit position in the given string
533 // or INVALID_MSD.
Hans Boehm08e8f322015-04-21 13:18:38 -0700534 public static int getMsdPos(String s) {
Hans Boehm84614952014-11-25 18:46:17 -0800535 int len = s.length();
536 int nonzeroPos = -1;
537 for (int i = 0; i < len; ++i) {
538 char c = s.charAt(i);
539 if (c != '-' && c != '.' && c != '0') {
540 nonzeroPos = i;
541 break;
542 }
543 }
544 if (nonzeroPos >= 0 &&
545 (nonzeroPos < len - 1 || s.charAt(nonzeroPos) != '1')) {
546 return nonzeroPos;
547 } else {
548 // Unknown, or could change on reevaluation
549 return INVALID_MSD;
550 }
551
552 }
553
554 // Return most significant digit position in the cache, if determined,
555 // INVALID_MSD ow.
556 // If unknown, and we've computed less than DESIRED_PREC,
557 // schedule reevaluation and redisplay, with higher precision.
558 int getMsd() {
559 if (mMsd != INVALID_MSD) return mMsd;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700560 if (mRatVal != null && mRatVal.signum() == 0) {
Hans Boehm84614952014-11-25 18:46:17 -0800561 return INVALID_MSD; // None exists
562 }
563 int res = INVALID_MSD;
564 if (mCache != null) {
565 res = getMsdPos(mCache);
566 }
567 if (res == INVALID_MSD && mEvaluator == null
568 && mCurrentReevaluator == null && mCacheDigs < MAX_MSD_PREC) {
569 // We assert that mCache is not null, since there is no
570 // evaluator running.
571 ensureCachePrec(MAX_MSD_PREC);
572 // Could reevaluate more incrementally, but we suspect that if
573 // we have to reevaluate at all, the result is probably zero.
574 }
575 return res;
576 }
577
578 // Return a string with n placeholder characters.
579 private String getPadding(int n) {
580 StringBuilder padding = new StringBuilder();
581 char something =
582 mCalculator.getResources()
583 .getString(R.string.guessed_digit).charAt(0);
584 for (int i = 0; i < n; ++i) {
585 padding.append(something);
586 }
587 return padding.toString();
588 }
589
590 // Return the number of zero characters at the beginning of s
591 private int leadingZeroes(String s) {
592 int res = 0;
593 int len = s.length();
594 for (res = 0; res < len && s.charAt(res) == '0'; ++res) {}
595 return res;
596 }
597
Hans Boehm08e8f322015-04-21 13:18:38 -0700598 private static final int MIN_DIGS = 5;
599 // Leave at least this many digits from the whole number
600 // part on the screen, to avoid silly displays like 1E1.
601 // Return result to exactly prec[0] digits to the right of the
602 // decimal point.
Hans Boehm84614952014-11-25 18:46:17 -0800603 // The result should be no longer than maxDigs.
Hans Boehm08e8f322015-04-21 13:18:38 -0700604 // No exponent or other indication of precision is added.
Hans Boehm84614952014-11-25 18:46:17 -0800605 // The result is returned immediately, based on the
606 // current cache contents, but it may contain question
607 // marks for unknown digits. It may also use uncertain
608 // digits within EXTRA_DIGITS. If either of those occurred,
609 // schedule a reevaluation and redisplay operation.
Hans Boehm08e8f322015-04-21 13:18:38 -0700610 // Uncertain digits never appear to the left of the decimal point.
Hans Boehm84614952014-11-25 18:46:17 -0800611 // digs may be negative to only retrieve digits to the left
Hans Boehm08e8f322015-04-21 13:18:38 -0700612 // of the decimal point. (prec[0] = 0 means we include
613 // the decimal point, but nothing to the right. prec[0] = -1
Hans Boehm84614952014-11-25 18:46:17 -0800614 // means we drop the decimal point and start at the ones
615 // position. Should not be invoked if mVal is null.
Hans Boehm08e8f322015-04-21 13:18:38 -0700616 // This essentially just returns a substring of the full result;
617 // a leading minus sign or leading digits can be dropped.
618 // Result uses US conventions; is NOT internationalized.
619 // We set negative[0] if the number as a whole is negative,
620 // since we may drop the minus sign.
621 // We set truncated[0] if leading nonzero digits were dropped.
622 // getRational() can be used to determine whether the result
623 // is exact, or whether we dropped trailing digits.
624 // If the requested prec[0] value is out of range, we update
625 // it in place and use the updated value.
626 public String getString(int[] prec, int maxDigs,
627 boolean[] truncated, boolean[] negative) {
628 int digs = prec[0];
Hans Boehm84614952014-11-25 18:46:17 -0800629 mLastDigs = digs;
630 // Make sure we eventually get a complete answer
631 ensureCachePrec(digs + EXTRA_DIGITS);
632 if (mCache == null) {
633 // Nothing to do now; seems to happen on rare occasion
Hans Boehm08e8f322015-04-21 13:18:38 -0700634 // with weird user input timing;
635 // Will repair itself in a jiffy.
Hans Boehm84614952014-11-25 18:46:17 -0800636 return getPadding(1);
637 }
638 // Compute an appropriate substring of mCache.
639 // We avoid returning a huge string to minimize string
640 // allocation during scrolling.
641 // Pad as needed.
Hans Boehm08e8f322015-04-21 13:18:38 -0700642 final int len = mCache.length();
643 final boolean myNegative = mCache.charAt(0) == '-';
644 negative[0] = myNegative;
645 // Don't scroll left past leftmost digits in mCache
646 // unless that still leaves an integer.
Hans Boehm84614952014-11-25 18:46:17 -0800647 int integralDigits = len - mCacheDigs;
648 // includes 1 for dec. pt
Hans Boehm08e8f322015-04-21 13:18:38 -0700649 if (myNegative) --integralDigits;
650 int minDigs = Math.min(-integralDigits + MIN_DIGS, -1);
651 digs = Math.max(digs, minDigs);
652 prec[0] = digs;
Hans Boehm84614952014-11-25 18:46:17 -0800653 int offset = mCacheDigs - digs; // trailing digits to drop
654 int deficit = 0; // The number of digits we're short
655 if (offset < 0) {
656 offset = 0;
657 deficit = Math.min(digs - mCacheDigs, maxDigs);
658 }
659 int endIndx = len - offset;
660 if (endIndx < 1) return " ";
661 int startIndx = (endIndx + deficit <= maxDigs) ?
662 0
663 : endIndx + deficit - maxDigs;
Hans Boehm08e8f322015-04-21 13:18:38 -0700664 truncated[0] = (startIndx > getMsd());
665 String res = mCache.substring(startIndx, endIndx);
Hans Boehm84614952014-11-25 18:46:17 -0800666 if (deficit > 0) {
667 res = res + getPadding(deficit);
668 // Since we always compute past the decimal point,
669 // this never fills in the spot where the decimal point
670 // should go, and the rest of this can treat the
671 // made-up symbols as though they were digits.
672 }
Hans Boehm84614952014-11-25 18:46:17 -0800673 return res;
674 }
675
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700676 // Return rational representation of current result, if any.
677 public BoundedRational getRational() {
678 return mRatVal;
679 }
680
Hans Boehm84614952014-11-25 18:46:17 -0800681 private void clearCache() {
682 mCache = null;
683 mCacheDigs = mCacheDigsReq = 0;
684 mMsd = INVALID_MSD;
685 }
686
687 void clear() {
688 mExpr.clear();
689 clearCache();
690 }
691
Hans Boehmc023b732015-04-29 11:30:47 -0700692 // Begin evaluation of result and display when ready.
693 // We assume this is called after each insertion and deletion.
694 // Thus if we are called twice with the same effective end of
695 // the formula, the evaluation is redundant.
Hans Boehm84614952014-11-25 18:46:17 -0800696 void evaluateAndShowResult() {
Hans Boehmc023b732015-04-29 11:30:47 -0700697 if (!mChangedValue) {
698 // Already done or in progress.
699 return;
700 }
701 cancelAll();
702 clearCache();
703 mEvaluator = new AsyncDisplayResult(mDegreeMode, false);
704 mEvaluator.execute();
Hans Boehm84614952014-11-25 18:46:17 -0800705 }
706
707 // Ensure that we either display a result or complain.
708 // Does not invalidate a previously computed cache.
Hans Boehmc023b732015-04-29 11:30:47 -0700709 // We presume that any prior result was computed using the same
710 // expression.
Hans Boehm84614952014-11-25 18:46:17 -0800711 void requireResult() {
Hans Boehmc023b732015-04-29 11:30:47 -0700712 if (mCache == null || mExpr.hasTrailingOperators()) {
Hans Boehm84614952014-11-25 18:46:17 -0800713 // Restart evaluator in requested mode, i.e. with
Hans Boehmc023b732015-04-29 11:30:47 -0700714 // longer timeout, not ignoring trailing operators.
Hans Boehm84614952014-11-25 18:46:17 -0800715 cancelAll();
Hans Boehmc023b732015-04-29 11:30:47 -0700716 clearCache();
Hans Boehm84614952014-11-25 18:46:17 -0800717 mEvaluator = new AsyncDisplayResult(mDegreeMode, true);
718 mEvaluator.execute();
719 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700720 // Notify immediately, reusing existing result.
Hans Boehm84614952014-11-25 18:46:17 -0800721 int dotPos = mCache.indexOf('.');
722 String truncatedWholePart = mCache.substring(0, dotPos);
723 mCalculator.onEvaluate(mLastDigs,truncatedWholePart);
724 }
725 }
726
727 // Cancel all current background tasks.
728 // Return true if we cancelled an initial evaluation,
729 // leaving the expression displayed.
730 boolean cancelAll() {
731 if (mCurrentReevaluator != null) {
Hans Boehm84614952014-11-25 18:46:17 -0800732 mCurrentReevaluator.cancel(true);
733 mCacheDigsReq = mCacheDigs;
734 // Backgound computation touches only constructive reals.
735 // OK not to wait.
736 mCurrentReevaluator = null;
737 }
738 if (mEvaluator != null) {
Hans Boehm84614952014-11-25 18:46:17 -0800739 mEvaluator.cancel(true);
740 // There seems to be no good way to wait for cancellation
741 // to complete, and the evaluation continues to look at
742 // mExpr, which we will again modify.
743 // Give ourselves a new copy to work on instead.
744 mExpr = (CalculatorExpr)mExpr.clone();
745 // Approximation of constructive reals should be thread-safe,
746 // so we can let that continue until it notices the cancellation.
747 mEvaluator = null;
748 return true;
749 }
750 return false;
751 }
752
753 void restoreInstanceState(DataInput in) {
754 try {
755 CalculatorExpr.initExprInput();
756 mDegreeMode = in.readBoolean();
757 mExpr = new CalculatorExpr(in);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700758 mSavedName = in.readUTF();
759 mSaved = new CalculatorExpr(in);
Hans Boehm84614952014-11-25 18:46:17 -0800760 } catch (IOException e) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700761 Log.v("Calculator", "Exception while restoring:\n" + e);
Hans Boehm84614952014-11-25 18:46:17 -0800762 }
763 }
764
765 void saveInstanceState(DataOutput out) {
766 try {
767 CalculatorExpr.initExprOutput();
768 out.writeBoolean(mDegreeMode);
769 mExpr.write(out);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700770 out.writeUTF(mSavedName);
771 mSaved.write(out);
Hans Boehm84614952014-11-25 18:46:17 -0800772 } catch (IOException e) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700773 Log.v("Calculator", "Exception while saving state:\n" + e);
Hans Boehm84614952014-11-25 18:46:17 -0800774 }
775 }
776
777 // Append a button press to the current expression.
Hans Boehmc023b732015-04-29 11:30:47 -0700778 // Return false if we rejected the insertion due to obvious
Hans Boehm84614952014-11-25 18:46:17 -0800779 // syntax issues, and the expression is unchanged.
780 // Return true otherwise.
781 boolean append(int id) {
Hans Boehmc023b732015-04-29 11:30:47 -0700782 mChangedValue = (KeyMaps.digVal(id) != KeyMaps.NOT_DIGIT
783 || KeyMaps.isSuffix(id)
784 || id == R.id.const_pi || id == R.id.const_e);
Hans Boehm84614952014-11-25 18:46:17 -0800785 return mExpr.add(id);
786 }
787
Hans Boehmc023b732015-04-29 11:30:47 -0700788 void delete() {
789 mChangedValue = true;
790 mExpr.delete();
791 }
792
Hans Boehmbfe8c222015-04-02 16:26:07 -0700793 void setDegreeMode(boolean degrees) {
Hans Boehmc023b732015-04-29 11:30:47 -0700794 mChangedValue = true;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700795 mDegreeMode = degrees;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700796 }
797
Hans Boehmbfe8c222015-04-02 16:26:07 -0700798 boolean getDegreeMode() {
799 return mDegreeMode;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700800 }
801
Hans Boehm84614952014-11-25 18:46:17 -0800802 // Abbreviate the current expression to a pre-evaluated
803 // expression node, which will display as a short number.
804 // This should not be called unless the expression was
805 // previously evaluated and produced a non-error result.
806 // Pre-evaluated expressions can never represent an
807 // expression for which evaluation to a constructive real
808 // diverges. Subsequent re-evaluation will also not diverge,
809 // though it may generate errors of various kinds.
810 // E.g. sqrt(-10^-1000)
811 void collapse () {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700812 BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
Hans Boehm84614952014-11-25 18:46:17 -0800813 CalculatorExpr abbrvExpr = mExpr.abbreviate(
Hans Boehm682ff5e2015-03-09 14:40:25 -0700814 mVal, mRatVal, mDegreeMode,
815 getShortString(mCache, intVal));
Hans Boehm84614952014-11-25 18:46:17 -0800816 clear();
817 mExpr.append(abbrvExpr);
818 }
819
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700820 // Same as above, but put result in mSaved, leaving mExpr alone.
821 // Return false if result is unavailable.
822 boolean collapseToSaved() {
823 if (mCache == null) return false;
824 BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
825 CalculatorExpr abbrvExpr = mExpr.abbreviate(
826 mVal, mRatVal, mDegreeMode,
827 getShortString(mCache, intVal));
828 mSaved.clear();
829 mSaved.append(abbrvExpr);
830 return true;
831 }
832
833 Uri uriForSaved() {
834 return new Uri.Builder().scheme("tag")
835 .encodedOpaquePart(mSavedName)
836 .build();
837 }
838
839 // Collapse the current expression to mSaved and return a URI
840 // describing this particular result, so that we can refer to it
841 // later.
842 Uri capture() {
843 if (!collapseToSaved()) return null;
844 // Generate a new (entirely private) URI for this result.
845 // Attempt to conform to RFC4151, though it's unclear it matters.
846 Date date = new Date();
847 TimeZone tz = TimeZone.getDefault();
848 DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
849 df.setTimeZone(tz);
850 String isoDate = df.format(new Date());
851 mSavedName = "calculator2.android.com," + isoDate + ":"
852 + (new Random().nextInt() & 0x3fffffff);
853 Uri tag = uriForSaved();
854 return tag;
855 }
856
857 boolean isLastSaved(Uri uri) {
858 return uri.equals(uriForSaved());
859 }
860
861 void addSaved() {
Hans Boehmed9e6782015-05-01 17:21:13 -0700862 mChangedValue = true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700863 mExpr.append(mSaved);
864 }
865
Hans Boehm84614952014-11-25 18:46:17 -0800866 // Retrieve the main expression being edited.
867 // It is the callee's reponsibility to call cancelAll to cancel
868 // ongoing concurrent computations before modifying the result.
869 // TODO: Perhaps add functionality so we can keep this private?
870 CalculatorExpr getExpr() {
871 return mExpr;
872 }
873
874}