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