Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 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 | |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 17 | package com.android.calculator2; |
| 18 | |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 19 | import android.app.AlertDialog; |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 20 | import android.content.Context; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 21 | import android.content.DialogInterface; |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 22 | import android.content.SharedPreferences; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 23 | import android.net.Uri; |
| 24 | import android.os.AsyncTask; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 25 | import android.os.Handler; |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 26 | import android.preference.PreferenceManager; |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 27 | import android.support.annotation.VisibleForTesting; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 28 | import android.util.Log; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 29 | |
| 30 | import com.hp.creals.CR; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 31 | |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 32 | import java.io.DataInput; |
| 33 | import java.io.DataOutput; |
| 34 | import java.io.IOException; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 35 | import java.math.BigInteger; |
Justin Klaassen | e2711cb | 2015-05-28 11:13:17 -0700 | [diff] [blame] | 36 | import java.text.DateFormat; |
| 37 | import java.text.SimpleDateFormat; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 38 | import java.util.Date; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 39 | import java.util.Random; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 40 | import java.util.TimeZone; |
| 41 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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 | */ |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 90 | class Evaluator { |
Justin Klaassen | ecc69fd | 2015-05-07 10:30:57 -0700 | [diff] [blame] | 91 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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 | |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 98 | private static final String KEY_PREF_DEGREE_MODE = "degree_mode"; |
| 99 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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. |
Hans Boehm | 08e8f32 | 2015-04-21 13:18:38 -0700 | [diff] [blame] | 103 | private static final int EXTRA_DIGITS = 20; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 104 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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. |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 121 | private static final int INIT_PREC = 50; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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. |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 130 | private static final int EXP_COST = 3; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 131 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 132 | private final Calculator mCalculator; |
| 133 | private final CalculatorResult mResult; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 134 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 135 | // The current caluclator expression. |
| 136 | private CalculatorExpr mExpr; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 137 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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. |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 146 | private boolean mChangedValue; |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 147 | |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 148 | private SharedPreferences mSharedPrefs; |
| 149 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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 | |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 181 | Evaluator(Calculator calculator, |
| 182 | CalculatorResult resultDisplay) { |
| 183 | mCalculator = calculator; |
| 184 | mResult = resultDisplay; |
| 185 | mExpr = new CalculatorExpr(); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 186 | mSaved = new CalculatorExpr(); |
| 187 | mSavedName = "none"; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 188 | mTimeoutHandler = new Handler(); |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 189 | |
| 190 | mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(calculator); |
| 191 | mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 192 | } |
| 193 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 194 | /** |
| 195 | * Result of initial asynchronous result computation. |
| 196 | * Represents either an error or a result computed to an initial evaluation precision. |
| 197 | */ |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 198 | private static class InitialResult { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 199 | public final int errorResourceId; // Error string or INVALID_RES_ID. |
| 200 | public final CR val; // Constructive real value. |
| 201 | public final BoundedRational ratVal; // Rational value or null. |
| 202 | public final String newResultString; // Null iff it can't be computed. |
| 203 | public final int newResultStringOffset; |
| 204 | public final int initDisplayOffset; |
| 205 | InitialResult(CR v, BoundedRational rv, String s, int p, int idp) { |
| 206 | errorResourceId = Calculator.INVALID_RES_ID; |
| 207 | val = v; |
| 208 | ratVal = rv; |
| 209 | newResultString = s; |
| 210 | newResultStringOffset = p; |
| 211 | initDisplayOffset = idp; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 212 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 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; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 220 | } |
| 221 | boolean isError() { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 222 | return errorResourceId != Calculator.INVALID_RES_ID; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 223 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 224 | } |
| 225 | |
| 226 | private void displayCancelledMessage() { |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 227 | new AlertDialog.Builder(mCalculator) |
| 228 | .setMessage(R.string.cancelled) |
Justin Klaassen | cc1e8e2 | 2015-06-04 18:25:46 -0700 | [diff] [blame] | 229 | .setPositiveButton(R.string.dismiss, |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 230 | new DialogInterface.OnClickListener() { |
| 231 | public void onClick(DialogInterface d, int which) { } |
| 232 | }) |
| 233 | .create() |
| 234 | .show(); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 235 | } |
| 236 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 237 | // 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 Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 239 | private final long MAX_TIMEOUT = 15000; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 240 | |
| 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 Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 247 | private final long QUICK_TIMEOUT = 1000; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 248 | |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 249 | 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 252 | |
| 253 | private void displayTimeoutMessage() { |
Justin Klaassen | cc1e8e2 | 2015-06-04 18:25:46 -0700 | [diff] [blame] | 254 | final AlertDialog.Builder builder = new AlertDialog.Builder(mCalculator) |
| 255 | .setMessage(R.string.timeout) |
| 256 | .setNegativeButton(R.string.dismiss, null /* listener */); |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 257 | if (mTimeout != MAX_TIMEOUT) { |
Justin Klaassen | cc1e8e2 | 2015-06-04 18:25:46 -0700 | [diff] [blame] | 258 | builder.setPositiveButton(R.string.ok_remove_timeout, |
| 259 | new DialogInterface.OnClickListener() { |
| 260 | public void onClick(DialogInterface d, int which) { |
| 261 | mTimeout = MAX_TIMEOUT; |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 262 | mMaxResultBits = MAX_MAX_RESULT_BITS; |
Justin Klaassen | cc1e8e2 | 2015-06-04 18:25:46 -0700 | [diff] [blame] | 263 | } |
| 264 | }); |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 265 | } |
Justin Klaassen | cc1e8e2 | 2015-06-04 18:25:46 -0700 | [diff] [blame] | 266 | builder.show(); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 267 | } |
| 268 | |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 269 | // 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 274 | class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 275 | private boolean mDm; // degrees |
| 276 | private boolean mRequired; // Result was requested by user. |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 277 | private boolean mQuiet; // Suppress cancellation message. |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 278 | private Runnable mTimeoutRunnable = null; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 279 | AsyncEvaluator(boolean dm, boolean required) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 280 | mDm = dm; |
| 281 | mRequired = required; |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 282 | mQuiet = !required; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 283 | } |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 284 | 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 Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 291 | if (mRequired) { |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 292 | suppressCancelMessage(); |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 293 | displayTimeoutMessage(); |
| 294 | } |
| 295 | } |
| 296 | } |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 297 | private void suppressCancelMessage() { |
| 298 | mQuiet = true; |
| 299 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 300 | @Override |
| 301 | protected void onPreExecute() { |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 302 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 311 | /** |
| 312 | * Is a computed result too big for decimal conversion? |
| 313 | */ |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 314 | private boolean isTooBig(CalculatorExpr.EvalResult res) { |
| 315 | int maxBits = mRequired ? mMaxResultBits : QUICK_MAX_RESULT_BITS; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 316 | if (res.ratVal != null) { |
| 317 | return res.ratVal.wholeNumberBits() > maxBits; |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 318 | } else { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 319 | return res.val.get_appr(maxBits).bitLength() > 2; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 320 | } |
| 321 | } |
| 322 | @Override |
| 323 | protected InitialResult doInBackground(Void... nothing) { |
| 324 | try { |
Hans Boehm | e855376 | 2015-06-26 17:56:52 -0700 | [diff] [blame] | 325 | CalculatorExpr.EvalResult res = mExpr.eval(mDm); |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 326 | if (isTooBig(res)) { |
| 327 | // Avoid starting a long uninterruptible decimal conversion. |
| 328 | return new InitialResult(R.string.timeout); |
| 329 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 330 | int precOffset = INIT_PREC; |
| 331 | String initResult = res.val.toString(precOffset); |
| 332 | int msd = getMsdIndexOf(initResult); |
| 333 | if (BoundedRational.asBigInteger(res.ratVal) == null |
Hans Boehm | 682ff5e | 2015-03-09 14:40:25 -0700 | [diff] [blame] | 334 | && msd == INVALID_MSD) { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 335 | precOffset = MAX_MSD_PREC_OFFSET; |
| 336 | initResult = res.val.toString(precOffset); |
| 337 | msd = getMsdIndexOf(initResult); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 338 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 339 | 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 346 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 347 | return new InitialResult(res.val, res.ratVal, |
| 348 | initResult, precOffset, initDisplayOffset); |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 349 | } catch (CalculatorExpr.SyntaxException e) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 350 | return new InitialResult(R.string.error_syntax); |
Hans Boehm | fbcef70 | 2015-04-27 18:07:47 -0700 | [diff] [blame] | 351 | } catch (BoundedRational.ZeroDivisionException e) { |
Hans Boehm | fbcef70 | 2015-04-27 18:07:47 -0700 | [diff] [blame] | 352 | return new InitialResult(R.string.error_zero_divide); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 353 | } catch(ArithmeticException e) { |
| 354 | return new InitialResult(R.string.error_nan); |
Hans Boehm | 19e93c9 | 2015-06-19 18:31:28 -0700 | [diff] [blame] | 355 | } catch(CR.PrecisionOverflowException e) { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 356 | // Extremely unlikely unless we're actually dividing by zero or the like. |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 357 | return new InitialResult(R.string.error_overflow); |
Hans Boehm | 19e93c9 | 2015-06-19 18:31:28 -0700 | [diff] [blame] | 358 | } catch(CR.AbortedException e) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 359 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 367 | if (result.errorResourceId == R.string.timeout) { |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 368 | if (mRequired) { |
| 369 | displayTimeoutMessage(); |
| 370 | } |
| 371 | mCalculator.onCancelled(); |
| 372 | } else { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 373 | mCalculator.onError(result.errorResourceId); |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 374 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 375 | return; |
| 376 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 377 | 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 Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 389 | // been conservative. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 390 | // 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 Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 397 | } else { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 398 | // They should be equal. But nothing horrible should happen if they're not. e.g. |
| 399 | // because CalculatorResult.MAX_WIDTH was too small. |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 400 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 401 | mCalculator.onEvaluate(initPrecOffset, msdIndex, leastDigOffset, truncatedWholePart); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 402 | } |
| 403 | @Override |
| 404 | protected void onCancelled(InitialResult result) { |
Hans Boehm | 82e5a2f | 2015-07-20 20:08:14 -0700 | [diff] [blame] | 405 | // Invoker resets mEvaluator. |
| 406 | mTimeoutHandler.removeCallbacks(mTimeoutRunnable); |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 407 | if (mRequired && !mQuiet) { |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 408 | displayCancelledMessage(); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 409 | } // Otherwise, if mRequired, timeout processing displayed message. |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 410 | mCalculator.onCancelled(); |
| 411 | // Just drop the evaluation; Leave expression displayed. |
| 412 | return; |
| 413 | } |
| 414 | } |
| 415 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 416 | /** |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 417 | * 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 449 | * 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 459 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 460 | /** |
| 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 481 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 482 | @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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 493 | mResultString = unflipZeroes(mResultString, mResultStringOffset, |
| 494 | result.newResultString, result.newResultStringOffset); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 495 | 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 510 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 516 | mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS; |
| 517 | if (mResultString != null) { |
| 518 | mResultStringOffsetReq += mResultStringOffsetReq / PRECOMPUTE_DIVISOR; |
Hans Boehm | e457912 | 2015-05-26 17:52:32 -0700 | [diff] [blame] | 519 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 520 | mCurrentReevaluator.execute(mResultStringOffsetReq); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 521 | } |
| 522 | |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 523 | /** |
| 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 527 | * @param decIndex Index of decimal point in cache. |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 528 | * @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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 532 | int getLsdOffset(BoundedRational ratVal, String cache, int decIndex) { |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 533 | if (ratVal != null && ratVal.signum() == 0) return Integer.MIN_VALUE; |
| 534 | int result = BoundedRational.digitsRequired(ratVal); |
| 535 | if (result == 0) { |
| 536 | int i; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 537 | for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { } |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 538 | result = i; |
| 539 | } |
| 540 | return result; |
| 541 | } |
| 542 | |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 543 | // 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 Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 547 | /** |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 548 | * Retrieve the preferred precision "offset" for the currently displayed result. |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 549 | * 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 553 | * @param lastDigitOffset Position of least significant digit (1 = tenths digit) |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 554 | * or Integer.MAX_VALUE. |
| 555 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 556 | 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 Boehm | 682ff5e | 2015-03-09 14:40:25 -0700 | [diff] [blame] | 560 | // Don't display decimal point if result is an integer. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 561 | if (lastDigitOffset == 0) { |
| 562 | lastDigitOffset = -1; |
| 563 | } |
| 564 | if (lastDigitOffset != Integer.MAX_VALUE) { |
| 565 | if (wholeSize <= lineLength && lastDigitOffset <= 0) { |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 566 | // Exact integer. Prefer to display as integer, without decimal point. |
| 567 | return -1; |
| 568 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 569 | if (lastDigitOffset >= 0 |
| 570 | && wholeSize + lastDigitOffset + 1 /* decimal pt. */ <= lineLength) { |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 571 | // Display full exact number wo scientific notation. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 572 | return lastDigitOffset; |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 573 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 574 | } |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 575 | if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) { |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 576 | // Display number without scientific notation. Treat leading zero as msd. |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 577 | msd = wholeSize - 1; |
| 578 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 579 | if (msd > wholeSize + MAX_MSD_PREC_OFFSET) { |
Hans Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 580 | // Display a probable but uncertain 0 as "0.000000000", |
Hans Boehm | 760a9dc | 2015-04-20 10:27:12 -0700 | [diff] [blame] | 581 | // 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 Boehm | a0e45f3 | 2015-05-30 13:20:35 -0700 | [diff] [blame] | 586 | // 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 590 | } |
| 591 | |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 592 | private static final int SHORT_TARGET_LENGTH = 8; |
| 593 | private static final String SHORT_UNCERTAIN_ZERO = "0.00000" + KeyMaps.ELLIPSIS; |
Hans Boehm | 013969e | 2015-04-13 20:29:47 -0700 | [diff] [blame] | 594 | |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 595 | /** |
| 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 604 | * @param lsdOffset Position of least significant digit in finite representation, |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 605 | * relative to decimal point, or MAX_VALUE. |
| 606 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 607 | private String getShortString(String cache, int msdIndex, int lsdOffset) { |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 608 | // 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 622 | if (lsdOffset < INIT_PREC) { |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 623 | return "0"; |
| 624 | } else { |
| 625 | return SHORT_UNCERTAIN_ZERO; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 626 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 627 | } |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 628 | // Avoid scientific notation for small numbers of zeros. |
| 629 | // Instead stretch significant digits to include decimal point. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 630 | if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH |
| 631 | && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) { |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 632 | // Whole number that fits in allotted space. |
| 633 | // CalculatorResult would not use scientific notation either. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 634 | lsdOffset = -1; |
Hans Boehm | 013969e | 2015-04-13 20:29:47 -0700 | [diff] [blame] | 635 | } |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 636 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 641 | } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2 |
| 642 | && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) { |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 643 | // 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 653 | 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 Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 657 | // 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 Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 662 | + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent; |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 663 | } |
| 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 Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 673 | + KeyMaps.ELLIPSIS + "E" + exponent; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 674 | } |
| 675 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 676 | /** |
| 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 684 | for (int i = 0; i < len; ++i) { |
| 685 | char c = s.charAt(i); |
| 686 | if (c != '-' && c != '.' && c != '0') { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 687 | nonzeroIndex = i; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 688 | break; |
| 689 | } |
| 690 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 691 | if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) { |
| 692 | return nonzeroIndex; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 693 | } else { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 694 | return INVALID_MSD; |
| 695 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 696 | } |
| 697 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 698 | /** |
| 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 702 | * Result is almost consistent through reevaluations: It may increase by one, once. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 703 | */ |
| 704 | private int getMsdIndex() { |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 705 | 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 Boehm | 682ff5e | 2015-03-09 14:40:25 -0700 | [diff] [blame] | 712 | if (mRatVal != null && mRatVal.signum() == 0) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 713 | return INVALID_MSD; // None exists |
| 714 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 715 | int result = INVALID_MSD; |
| 716 | if (mResultString != null) { |
| 717 | result = getMsdIndexOf(mResultString); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 718 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 719 | return result; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 720 | } |
| 721 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 722 | /** |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 723 | * Return a string with n copies of c. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 724 | */ |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 725 | private static String repeat(char c, int n) { |
| 726 | StringBuilder result = new StringBuilder(); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 727 | for (int i = 0; i < n; ++i) { |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 728 | result.append(c); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 729 | } |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 730 | return result.toString(); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 731 | } |
| 732 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 733 | // 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 754 | * @param maxDigs Maximum length of result |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 755 | * @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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 761 | // Make sure we eventually get a complete answer |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 762 | 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 766 | return " "; |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 767 | } 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 781 | } |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 782 | 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 800 | result += repeat(' ', deficit); |
| 801 | // Blank character is replaced during translation. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 802 | // 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 807 | } |
| 808 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 809 | /** |
| 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 Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 814 | public BoundedRational getRational() { |
| 815 | return mRatVal; |
| 816 | } |
| 817 | |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 818 | private void clearCache() { |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 819 | mResultString = null; |
| 820 | mResultStringOffset = mResultStringOffsetReq = 0; |
| 821 | mMsdIndex = INVALID_MSD; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 822 | } |
| 823 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 824 | public void clear() { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 825 | mExpr.clear(); |
| 826 | clearCache(); |
| 827 | } |
| 828 | |
Hans Boehm | ae807e1 | 2015-07-14 15:40:52 -0700 | [diff] [blame] | 829 | /** |
| 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 834 | private void evaluateResult(boolean required) { |
Hans Boehm | ae807e1 | 2015-07-14 15:40:52 -0700 | [diff] [blame] | 835 | clearCache(); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 836 | mEvaluator = new AsyncEvaluator(mDegreeMode, required); |
Hans Boehm | ae807e1 | 2015-07-14 15:40:52 -0700 | [diff] [blame] | 837 | mEvaluator.execute(); |
| 838 | mChangedValue = false; |
| 839 | } |
| 840 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 841 | /** |
| 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 Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 846 | if (!mChangedValue) { |
| 847 | // Already done or in progress. |
| 848 | return; |
| 849 | } |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 850 | // In very odd cases, there can be significant latency to evaluate. |
| 851 | // Don't show obsolete result. |
| 852 | mResult.clear(); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 853 | evaluateResult(false); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 854 | } |
| 855 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 856 | /** |
| 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 860 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 861 | public void requireResult() { |
| 862 | if (mResultString == null || mChangedValue) { |
Hans Boehm | e855376 | 2015-06-26 17:56:52 -0700 | [diff] [blame] | 863 | // Restart evaluator in requested mode, i.e. with longer timeout. |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 864 | cancelAll(true); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 865 | evaluateResult(true); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 866 | } else { |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 867 | // Notify immediately, reusing existing result. |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 868 | 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 Boehm | 15a853d | 2015-06-23 18:32:02 -0700 | [diff] [blame] | 874 | mCalculator.onEvaluate(preferredPrecOffset, msdIndex, leastDigOffset, |
| 875 | truncatedWholePart); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 876 | } |
| 877 | } |
| 878 | |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 879 | /** |
| 880 | * Cancel all current background tasks. |
| 881 | * @param quiet suppress cancellation message |
| 882 | * @return true if we cancelled an initial evaluation |
| 883 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 884 | public boolean cancelAll(boolean quiet) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 885 | if (mCurrentReevaluator != null) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 886 | mCurrentReevaluator.cancel(true); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 887 | mResultStringOffsetReq = mResultStringOffset; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 888 | // Backgound computation touches only constructive reals. |
| 889 | // OK not to wait. |
| 890 | mCurrentReevaluator = null; |
| 891 | } |
| 892 | if (mEvaluator != null) { |
Hans Boehm | c1ea091 | 2015-06-19 15:05:07 -0700 | [diff] [blame] | 893 | if (quiet) { |
| 894 | mEvaluator.suppressCancelMessage(); |
| 895 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 896 | 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 Boehm | 187d3e9 | 2015-06-09 18:04:26 -0700 | [diff] [blame] | 905 | mChangedValue = true; // Didn't do the expected evaluation. |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 906 | return true; |
| 907 | } |
| 908 | return false; |
| 909 | } |
| 910 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 911 | /** |
| 912 | * Restore the evaluator state, including the expression and any saved value. |
| 913 | */ |
| 914 | public void restoreInstanceState(DataInput in) { |
Hans Boehm | 1176f23 | 2015-05-11 16:26:03 -0700 | [diff] [blame] | 915 | mChangedValue = true; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 916 | try { |
| 917 | CalculatorExpr.initExprInput(); |
| 918 | mDegreeMode = in.readBoolean(); |
| 919 | mExpr = new CalculatorExpr(in); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 920 | mSavedName = in.readUTF(); |
| 921 | mSaved = new CalculatorExpr(in); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 922 | } catch (IOException e) { |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 923 | Log.v("Calculator", "Exception while restoring:\n" + e); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 924 | } |
| 925 | } |
| 926 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 927 | /** |
| 928 | * Save the evaluator state, including the expression and any saved value. |
| 929 | */ |
| 930 | public void saveInstanceState(DataOutput out) { |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 931 | try { |
| 932 | CalculatorExpr.initExprOutput(); |
| 933 | out.writeBoolean(mDegreeMode); |
| 934 | mExpr.write(out); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 935 | out.writeUTF(mSavedName); |
| 936 | mSaved.write(out); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 937 | } catch (IOException e) { |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 938 | Log.v("Calculator", "Exception while saving state:\n" + e); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 939 | } |
| 940 | } |
| 941 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 942 | |
| 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 Boehm | 4db31b4 | 2015-05-31 12:19:05 -0700 | [diff] [blame] | 950 | if (id == R.id.fun_10pow) { |
| 951 | add10pow(); // Handled as macro expansion. |
| 952 | return true; |
| 953 | } else { |
Hans Boehm | e855376 | 2015-06-26 17:56:52 -0700 | [diff] [blame] | 954 | mChangedValue = mChangedValue || !KeyMaps.isBinary(id); |
Hans Boehm | 4db31b4 | 2015-05-31 12:19:05 -0700 | [diff] [blame] | 955 | return mExpr.add(id); |
| 956 | } |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 957 | } |
| 958 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 959 | public void delete() { |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 960 | mChangedValue = true; |
| 961 | mExpr.delete(); |
| 962 | } |
| 963 | |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 964 | void setDegreeMode(boolean degreeMode) { |
Hans Boehm | c023b73 | 2015-04-29 11:30:47 -0700 | [diff] [blame] | 965 | mChangedValue = true; |
Justin Klaassen | 8b1efdb | 2015-06-22 15:10:53 -0700 | [diff] [blame] | 966 | mDegreeMode = degreeMode; |
| 967 | |
| 968 | mSharedPrefs.edit() |
| 969 | .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode) |
| 970 | .apply(); |
Hans Boehm | 682ff5e | 2015-03-09 14:40:25 -0700 | [diff] [blame] | 971 | } |
| 972 | |
Hans Boehm | bfe8c22 | 2015-04-02 16:26:07 -0700 | [diff] [blame] | 973 | boolean getDegreeMode() { |
| 974 | return mDegreeMode; |
Hans Boehm | 682ff5e | 2015-03-09 14:40:25 -0700 | [diff] [blame] | 975 | } |
| 976 | |
Justin Klaassen | 4459516 | 2015-05-28 17:55:20 -0700 | [diff] [blame] | 977 | /** |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 978 | * @return the {@link CalculatorExpr} representation of the current result. |
Justin Klaassen | 4459516 | 2015-05-28 17:55:20 -0700 | [diff] [blame] | 979 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 980 | private CalculatorExpr getResultExpr() { |
| 981 | final int dotIndex = mResultString.indexOf('.'); |
| 982 | final int leastDigOffset = getLsdOffset(mRatVal, mResultString, dotIndex); |
Hans Boehm | 50ed320 | 2015-06-09 14:35:49 -0700 | [diff] [blame] | 983 | return mExpr.abbreviate(mVal, mRatVal, mDegreeMode, |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 984 | getShortString(mResultString, getMsdIndexOf(mResultString), leastDigOffset)); |
Justin Klaassen | 4459516 | 2015-05-28 17:55:20 -0700 | [diff] [blame] | 985 | } |
| 986 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 987 | /** |
| 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 Klaassen | 4459516 | 2015-05-28 17:55:20 -0700 | [diff] [blame] | 995 | final CalculatorExpr abbrvExpr = getResultExpr(); |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 996 | clear(); |
| 997 | mExpr.append(abbrvExpr); |
Hans Boehm | 187d3e9 | 2015-06-09 18:04:26 -0700 | [diff] [blame] | 998 | mChangedValue = true; |
Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 999 | } |
| 1000 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1001 | /** |
| 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 Klaassen | 4459516 | 2015-05-28 17:55:20 -0700 | [diff] [blame] | 1007 | return false; |
| 1008 | } |
Justin Klaassen | 4459516 | 2015-05-28 17:55:20 -0700 | [diff] [blame] | 1009 | final CalculatorExpr abbrvExpr = getResultExpr(); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1010 | mSaved.clear(); |
| 1011 | mSaved.append(abbrvExpr); |
| 1012 | return true; |
| 1013 | } |
| 1014 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1015 | private Uri uriForSaved() { |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1016 | return new Uri.Builder().scheme("tag") |
| 1017 | .encodedOpaquePart(mSavedName) |
| 1018 | .build(); |
| 1019 | } |
| 1020 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1021 | /** |
| 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 Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1027 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1030 | final TimeZone tz = TimeZone.getDefault(); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1031 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); |
| 1032 | df.setTimeZone(tz); |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1033 | final String isoDate = df.format(new Date()); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1034 | mSavedName = "calculator2.android.com," + isoDate + ":" |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1035 | + (new Random().nextInt() & 0x3fffffff); |
| 1036 | return uriForSaved(); |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1037 | } |
| 1038 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1039 | public boolean isLastSaved(Uri uri) { |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1040 | return uri.equals(uriForSaved()); |
| 1041 | } |
| 1042 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1043 | public void appendSaved() { |
Hans Boehm | ed9e678 | 2015-05-01 17:21:13 -0700 | [diff] [blame] | 1044 | mChangedValue = true; |
Hans Boehm | 4a6b7cb | 2015-04-03 18:41:52 -0700 | [diff] [blame] | 1045 | mExpr.append(mSaved); |
| 1046 | } |
| 1047 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1048 | /** |
| 1049 | * Add the power of 10 operator to the expression. |
| 1050 | * This is treated essentially as a macro expansion. |
| 1051 | */ |
Hans Boehm | 4db31b4 | 2015-05-31 12:19:05 -0700 | [diff] [blame] | 1052 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1061 | /** |
| 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 1071 | return mExpr; |
| 1072 | } |
| 1073 | |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1074 | /** |
| 1075 | * Maximum number of characters in a scientific notation exponent. |
| 1076 | */ |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1077 | 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1082 | * Exponents have syntax E[-]digit* . "E2" and "E-2" are valid. "E+2" and "e2" are not. |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1083 | * We allow any Unicode digits, and either of the commonly used minus characters. |
| 1084 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1085 | public static int exponentEnd(String s, int offset) { |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1086 | 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 Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 1095 | if (i == len || !Character.isDigit(s.charAt(i))) { |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1096 | return offset; |
| 1097 | } |
| 1098 | ++i; |
| 1099 | while (i < len && Character.isDigit(s.charAt(i))) { |
| 1100 | ++i; |
Hans Boehm | 79302b0 | 2015-08-05 17:20:32 -0700 | [diff] [blame^] | 1101 | if (i > offset + MAX_EXP_CHARS) { |
| 1102 | return offset; |
| 1103 | } |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1104 | } |
| 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 Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1111 | * The end of the current expression must be a constant. Exponents have the same syntax as |
| 1112 | * for exponentEnd(). |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1113 | */ |
Hans Boehm | 3666e63 | 2015-07-27 18:33:12 -0700 | [diff] [blame] | 1114 | public void addExponent(String s, int begin, int end) { |
Hans Boehm | 0b9806f | 2015-06-29 16:07:15 -0700 | [diff] [blame] | 1115 | 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 Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame] | 1130 | } |