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 20 import com.hp.creals.CR; 21 import com.hp.creals.UnaryCRFunction; 22 23 import android.content.Context; 24 import android.text.SpannableString; 25 import android.text.SpannableStringBuilder; 26 import android.text.Spanned; 27 import android.text.style.TtsSpan; 28 import android.text.style.TtsSpan.TextBuilder; 29 import android.util.Log; 30 31 import java.math.BigInteger; 32 import java.io.DataInput; 33 import java.io.DataOutput; 34 import java.io.IOException; 35 import java.util.ArrayList; 36 import java.util.HashMap; 37 import java.util.IdentityHashMap; 38 39 /** 40 * A mathematical expression represented as a sequence of "tokens". 41 * Many tokens are represented by button ids for the corresponding operator. 42 * A token may also represent the result of a previously evaluated expression. 43 * The add() method adds a token to the end of the expression. The delete method() removes one. 44 * Clear() deletes the entire expression contents. Eval() evaluates the expression, 45 * producing both a constructive real (CR), and possibly a BoundedRational result. 46 * Expressions are parsed only during evaluation; no explicit parse tree is maintained. 47 * 48 * The write() method is used to save the current expression. Note that CR provides no 49 * serialization facility. Thus we save all previously computed values by writing out the 50 * expression that was used to compute them, and reevaluate on input. 51 */ 52 class CalculatorExpr { 53 private ArrayList<Token> mExpr; // The actual representation 54 // as a list of tokens. Constant 55 // tokens are always nonempty. 56 57 private static enum TokenKind { CONSTANT, OPERATOR, PRE_EVAL }; 58 private static TokenKind[] tokenKindValues = TokenKind.values(); 59 private final static BigInteger BIG_MILLION = BigInteger.valueOf(1000000); 60 private final static BigInteger BIG_BILLION = BigInteger.valueOf(1000000000); 61 62 private static abstract class Token { kind()63 abstract TokenKind kind(); 64 65 /** 66 * Write kind as Byte followed by data needed by subclass constructor. 67 */ write(DataOutput out)68 abstract void write(DataOutput out) throws IOException; 69 70 /** 71 * Return a textual representation of the token. 72 * The result is suitable for either display as part od the formula or TalkBack use. 73 * It may be a SpannableString that includes added TalkBack information. 74 * @param context context used for converting button ids to strings 75 */ toCharSequence(Context context)76 abstract CharSequence toCharSequence(Context context); 77 } 78 79 /** 80 * Representation of an operator token 81 */ 82 private static class Operator extends Token { 83 public final int id; // We use the button resource id Operator(int resId)84 Operator(int resId) { 85 id = resId; 86 } Operator(DataInput in)87 Operator(DataInput in) throws IOException { 88 id = in.readInt(); 89 } 90 @Override write(DataOutput out)91 void write(DataOutput out) throws IOException { 92 out.writeByte(TokenKind.OPERATOR.ordinal()); 93 out.writeInt(id); 94 } 95 @Override toCharSequence(Context context)96 public CharSequence toCharSequence(Context context) { 97 String desc = KeyMaps.toDescriptiveString(context, id); 98 if (desc != null) { 99 SpannableString result = new SpannableString(KeyMaps.toString(context, id)); 100 Object descSpan = new TtsSpan.TextBuilder(desc).build(); 101 result.setSpan(descSpan, 0, result.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 102 return result; 103 } else { 104 return KeyMaps.toString(context, id); 105 } 106 } 107 @Override kind()108 TokenKind kind() { return TokenKind.OPERATOR; } 109 } 110 111 /** 112 * Representation of a (possibly incomplete) numerical constant. 113 * Supports addition and removal of trailing characters; hence mutable. 114 */ 115 private static class Constant extends Token implements Cloneable { 116 private boolean mSawDecimal; 117 private String mWhole; // String preceding decimal point. 118 private String mFraction; // String after decimal point. 119 private int mExponent; // Explicit exponent, only generated through addExponent. 120 Constant()121 Constant() { 122 mWhole = ""; 123 mFraction = ""; 124 mSawDecimal = false; 125 mExponent = 0; 126 }; 127 Constant(DataInput in)128 Constant(DataInput in) throws IOException { 129 mWhole = in.readUTF(); 130 mSawDecimal = in.readBoolean(); 131 mFraction = in.readUTF(); 132 mExponent = in.readInt(); 133 } 134 135 @Override write(DataOutput out)136 void write(DataOutput out) throws IOException { 137 out.writeByte(TokenKind.CONSTANT.ordinal()); 138 out.writeUTF(mWhole); 139 out.writeBoolean(mSawDecimal); 140 out.writeUTF(mFraction); 141 out.writeInt(mExponent); 142 } 143 144 // Given a button press, append corresponding digit. 145 // We assume id is a digit or decimal point. 146 // Just return false if this was the second (or later) decimal point 147 // in this constant. 148 // Assumes that this constant does not have an exponent. add(int id)149 public boolean add(int id) { 150 if (id == R.id.dec_point) { 151 if (mSawDecimal || mExponent != 0) return false; 152 mSawDecimal = true; 153 return true; 154 } 155 int val = KeyMaps.digVal(id); 156 if (mExponent != 0) { 157 if (Math.abs(mExponent) <= 10000) { 158 if (mExponent > 0) { 159 mExponent = 10 * mExponent + val; 160 } else { 161 mExponent = 10 * mExponent - val; 162 } 163 return true; 164 } else { // Too large; refuse 165 return false; 166 } 167 } 168 if (mSawDecimal) { 169 mFraction += val; 170 } else { 171 mWhole += val; 172 } 173 return true; 174 } 175 addExponent(int exp)176 public void addExponent(int exp) { 177 // Note that adding a 0 exponent is a no-op. That's OK. 178 mExponent = exp; 179 } 180 181 /** 182 * Undo the last add or remove last exponent digit. 183 * Assumes the constant is nonempty. 184 */ delete()185 public void delete() { 186 if (mExponent != 0) { 187 mExponent /= 10; 188 // Once zero, it can only be added back with addExponent. 189 } else if (!mFraction.isEmpty()) { 190 mFraction = mFraction.substring(0, mFraction.length() - 1); 191 } else if (mSawDecimal) { 192 mSawDecimal = false; 193 } else { 194 mWhole = mWhole.substring(0, mWhole.length() - 1); 195 } 196 } 197 isEmpty()198 public boolean isEmpty() { 199 return (mSawDecimal == false && mWhole.isEmpty()); 200 } 201 202 /** 203 * Produce human-readable string representation of constant, as typed. 204 * Result is internationalized. 205 */ 206 @Override toString()207 public String toString() { 208 String result = mWhole; 209 if (mSawDecimal) { 210 result += '.'; 211 result += mFraction; 212 } 213 if (mExponent != 0) { 214 result += "E" + mExponent; 215 } 216 return KeyMaps.translateResult(result); 217 } 218 219 /** 220 * Return BoundedRational representation of constant, if well-formed. 221 * Result is never null. 222 */ toRational()223 public BoundedRational toRational() throws SyntaxException { 224 String whole = mWhole; 225 if (whole.isEmpty()) { 226 if (mFraction.isEmpty()) { 227 // Decimal point without digits. 228 throw new SyntaxException(); 229 } else { 230 whole = "0"; 231 } 232 } 233 BigInteger num = new BigInteger(whole + mFraction); 234 BigInteger den = BigInteger.TEN.pow(mFraction.length()); 235 if (mExponent > 0) { 236 num = num.multiply(BigInteger.TEN.pow(mExponent)); 237 } 238 if (mExponent < 0) { 239 den = den.multiply(BigInteger.TEN.pow(-mExponent)); 240 } 241 return new BoundedRational(num, den); 242 } 243 244 @Override toCharSequence(Context context)245 public CharSequence toCharSequence(Context context) { 246 return toString(); 247 } 248 249 @Override kind()250 public TokenKind kind() { 251 return TokenKind.CONSTANT; 252 } 253 254 // Override clone to make it public 255 @Override clone()256 public Object clone() { 257 Constant result = new Constant(); 258 result.mWhole = mWhole; 259 result.mFraction = mFraction; 260 result.mSawDecimal = mSawDecimal; 261 result.mExponent = mExponent; 262 return result; 263 } 264 } 265 266 // Hash maps used to detect duplicate subexpressions when we write out CalculatorExprs and 267 // read them back in. 268 private static final ThreadLocal<IdentityHashMap<CR,Integer>>outMap = 269 new ThreadLocal<IdentityHashMap<CR,Integer>>(); 270 // Maps expressions to indices on output 271 private static final ThreadLocal<HashMap<Integer,PreEval>>inMap = 272 new ThreadLocal<HashMap<Integer,PreEval>>(); 273 // Maps expressions to indices on output 274 private static final ThreadLocal<Integer> exprIndex = new ThreadLocal<Integer>(); 275 276 /** 277 * Prepare for expression output. 278 * Initializes map that will lbe used to avoid duplicating shared subexpressions. 279 * This avoids a potential exponential blow-up in the expression size. 280 */ initExprOutput()281 public static void initExprOutput() { 282 outMap.set(new IdentityHashMap<CR,Integer>()); 283 exprIndex.set(Integer.valueOf(0)); 284 } 285 286 /** 287 * Prepare for expression input. 288 * Initializes map that will be used to reconstruct shared subexpressions. 289 */ initExprInput()290 public static void initExprInput() { 291 inMap.set(new HashMap<Integer,PreEval>()); 292 } 293 294 /** 295 * The "token" class for previously evaluated subexpressions. 296 * We treat previously evaluated subexpressions as tokens. These are inserted when we either 297 * continue an expression after evaluating some of it, or copy an expression and paste it back 298 * in. 299 * The representation includes both CR and possibly BoundedRational values. In order to 300 * support saving and restoring, we also include the underlying expression itself, and the 301 * context (currently just degree mode) used to evaluate it. The short string representation 302 * is also stored in order to avoid potentially expensive recomputation in the UI thread. 303 */ 304 private static class PreEval extends Token { 305 public final CR value; 306 public final BoundedRational ratValue; 307 private final CalculatorExpr mExpr; 308 private final EvalContext mContext; 309 private final String mShortRep; // Not internationalized. PreEval(CR val, BoundedRational ratVal, CalculatorExpr expr, EvalContext ec, String shortRep)310 PreEval(CR val, BoundedRational ratVal, CalculatorExpr expr, 311 EvalContext ec, String shortRep) { 312 value = val; 313 ratValue = ratVal; 314 mExpr = expr; 315 mContext = ec; 316 mShortRep = shortRep; 317 } 318 // In writing out PreEvals, we are careful to avoid writing 319 // out duplicates. We assume that two expressions are 320 // duplicates if they have the same CR value. This avoids a 321 // potential exponential blow up in certain off cases and 322 // redundant evaluation after reading them back in. 323 // The parameter hash map maps expressions we've seen 324 // before to their index. 325 @Override write(DataOutput out)326 public void write(DataOutput out) throws IOException { 327 out.writeByte(TokenKind.PRE_EVAL.ordinal()); 328 Integer index = outMap.get().get(value); 329 if (index == null) { 330 int nextIndex = exprIndex.get() + 1; 331 exprIndex.set(nextIndex); 332 outMap.get().put(value, nextIndex); 333 out.writeInt(nextIndex); 334 mExpr.write(out); 335 mContext.write(out); 336 out.writeUTF(mShortRep); 337 } else { 338 // Just write out the index 339 out.writeInt(index); 340 } 341 } PreEval(DataInput in)342 PreEval(DataInput in) throws IOException { 343 int index = in.readInt(); 344 PreEval prev = inMap.get().get(index); 345 if (prev == null) { 346 mExpr = new CalculatorExpr(in); 347 mContext = new EvalContext(in, mExpr.mExpr.size()); 348 // Recompute other fields We currently do this in the UI thread, but we only 349 // create PreEval expressions that were previously successfully evaluated, and 350 // thus don't diverge. We also only evaluate to a constructive real, which 351 // involves substantial work only in fairly contrived circumstances. 352 // TODO: Deal better with slow evaluations. 353 EvalRet res = null; 354 try { 355 res = mExpr.evalExpr(0, mContext); 356 } catch (SyntaxException e) { 357 // Should be impossible, since we only write out 358 // expressions that can be evaluated. 359 Log.e("Calculator", "Unexpected syntax exception" + e); 360 } 361 value = res.val; 362 ratValue = res.ratVal; 363 mShortRep = in.readUTF(); 364 inMap.get().put(index, this); 365 } else { 366 value = prev.value; 367 ratValue = prev.ratValue; 368 mExpr = prev.mExpr; 369 mContext = prev.mContext; 370 mShortRep = prev.mShortRep; 371 } 372 } 373 @Override toCharSequence(Context context)374 public CharSequence toCharSequence(Context context) { 375 return KeyMaps.translateResult(mShortRep); 376 } 377 @Override kind()378 public TokenKind kind() { 379 return TokenKind.PRE_EVAL; 380 } hasEllipsis()381 public boolean hasEllipsis() { 382 return mShortRep.lastIndexOf(KeyMaps.ELLIPSIS) != -1; 383 } 384 } 385 386 /** 387 * Read token from in. 388 */ newToken(DataInput in)389 public static Token newToken(DataInput in) throws IOException { 390 TokenKind kind = tokenKindValues[in.readByte()]; 391 switch(kind) { 392 case CONSTANT: 393 return new Constant(in); 394 case OPERATOR: 395 return new Operator(in); 396 case PRE_EVAL: 397 return new PreEval(in); 398 default: throw new IOException("Bad save file format"); 399 } 400 } 401 CalculatorExpr()402 CalculatorExpr() { 403 mExpr = new ArrayList<Token>(); 404 } 405 CalculatorExpr(ArrayList<Token> expr)406 private CalculatorExpr(ArrayList<Token> expr) { 407 mExpr = expr; 408 } 409 410 /** 411 * Construct CalculatorExpr, by reading it from in. 412 */ CalculatorExpr(DataInput in)413 CalculatorExpr(DataInput in) throws IOException { 414 mExpr = new ArrayList<Token>(); 415 int size = in.readInt(); 416 for (int i = 0; i < size; ++i) { 417 mExpr.add(newToken(in)); 418 } 419 } 420 421 /** 422 * Write this expression to out. 423 */ write(DataOutput out)424 public void write(DataOutput out) throws IOException { 425 int size = mExpr.size(); 426 out.writeInt(size); 427 for (int i = 0; i < size; ++i) { 428 mExpr.get(i).write(out); 429 } 430 } 431 432 /** 433 * Does this expression end with a numeric constant? 434 * As opposed to an operator or preevaluated expression. 435 */ hasTrailingConstant()436 boolean hasTrailingConstant() { 437 int s = mExpr.size(); 438 if (s == 0) { 439 return false; 440 } 441 Token t = mExpr.get(s-1); 442 return t instanceof Constant; 443 } 444 445 /** 446 * Does this expression end with a binary operator? 447 */ hasTrailingBinary()448 private boolean hasTrailingBinary() { 449 int s = mExpr.size(); 450 if (s == 0) return false; 451 Token t = mExpr.get(s-1); 452 if (!(t instanceof Operator)) return false; 453 Operator o = (Operator)t; 454 return (KeyMaps.isBinary(o.id)); 455 } 456 457 /** 458 * Append press of button with given id to expression. 459 * If the insertion would clearly result in a syntax error, either just return false 460 * and do nothing, or make an adjustment to avoid the problem. We do the latter only 461 * for unambiguous consecutive binary operators, in which case we delete the first 462 * operator. 463 */ add(int id)464 boolean add(int id) { 465 int s = mExpr.size(); 466 final int d = KeyMaps.digVal(id); 467 final boolean binary = KeyMaps.isBinary(id); 468 Token lastTok = s == 0 ? null : mExpr.get(s-1); 469 int lastOp = lastTok instanceof Operator ? ((Operator) lastTok).id : 0; 470 // Quietly replace a trailing binary operator with another one, unless the second 471 // operator is minus, in which case we just allow it as a unary minus. 472 if (binary && !KeyMaps.isPrefix(id)) { 473 if (s == 0 || lastOp == R.id.lparen || KeyMaps.isFunc(lastOp) 474 || KeyMaps.isPrefix(lastOp) && lastOp != R.id.op_sub) { 475 return false; 476 } 477 while (hasTrailingBinary()) { 478 delete(); 479 } 480 // s invalid and not used below. 481 } 482 final boolean isConstPiece = (d != KeyMaps.NOT_DIGIT || id == R.id.dec_point); 483 if (isConstPiece) { 484 // Since we treat juxtaposition as multiplication, a constant can appear anywhere. 485 if (s == 0) { 486 mExpr.add(new Constant()); 487 s++; 488 } else { 489 Token last = mExpr.get(s-1); 490 if(!(last instanceof Constant)) { 491 if (last instanceof PreEval) { 492 // Add explicit multiplication to avoid confusing display. 493 mExpr.add(new Operator(R.id.op_mul)); 494 s++; 495 } 496 mExpr.add(new Constant()); 497 s++; 498 } 499 } 500 return ((Constant)(mExpr.get(s-1))).add(id); 501 } else { 502 mExpr.add(new Operator(id)); 503 return true; 504 } 505 } 506 507 /** 508 * Add exponent to the constant at the end of the expression. 509 * Assumes there is a constant at the end of the expression. 510 */ addExponent(int exp)511 void addExponent(int exp) { 512 Token lastTok = mExpr.get(mExpr.size() - 1); 513 ((Constant) lastTok).addExponent(exp); 514 } 515 516 /** 517 * Remove trailing op_add and op_sub operators. 518 */ removeTrailingAdditiveOperators()519 void removeTrailingAdditiveOperators() { 520 while (true) { 521 int s = mExpr.size(); 522 if (s == 0) { 523 break; 524 } 525 Token lastTok = mExpr.get(s-1); 526 if (!(lastTok instanceof Operator)) { 527 break; 528 } 529 int lastOp = ((Operator) lastTok).id; 530 if (lastOp != R.id.op_add && lastOp != R.id.op_sub) { 531 break; 532 } 533 delete(); 534 } 535 } 536 537 /** 538 * Append the contents of the argument expression. 539 * It is assumed that the argument expression will not change, and thus its pieces can be 540 * reused directly. 541 */ append(CalculatorExpr expr2)542 public void append(CalculatorExpr expr2) { 543 int s = mExpr.size(); 544 int s2 = expr2.mExpr.size(); 545 // Check that we're not concatenating Constant or PreEval tokens, since the result would 546 // look like a single constant, with very mysterious results for the user. 547 if (s != 0 && s2 != 0) { 548 Token last = mExpr.get(s-1); 549 Token first = expr2.mExpr.get(0); 550 if (!(first instanceof Operator) && !(last instanceof Operator)) { 551 // Fudge it by adding an explicit multiplication. We would have interpreted it as 552 // such anyway, and this makes it recognizable to the user. 553 mExpr.add(new Operator(R.id.op_mul)); 554 } 555 } 556 for (int i = 0; i < s2; ++i) { 557 mExpr.add(expr2.mExpr.get(i)); 558 } 559 } 560 561 /** 562 * Undo the last key addition, if any. 563 * Or possibly remove a trailing exponent digit. 564 */ delete()565 public void delete() { 566 final int s = mExpr.size(); 567 if (s == 0) { 568 return; 569 } 570 Token last = mExpr.get(s-1); 571 if (last instanceof Constant) { 572 Constant c = (Constant)last; 573 c.delete(); 574 if (!c.isEmpty()) { 575 return; 576 } 577 } 578 mExpr.remove(s-1); 579 } 580 581 /** 582 * Remove all tokens from the expression. 583 */ clear()584 public void clear() { 585 mExpr.clear(); 586 } 587 isEmpty()588 public boolean isEmpty() { 589 return mExpr.isEmpty(); 590 } 591 592 /** 593 * Returns a logical deep copy of the CalculatorExpr. 594 * Operator and PreEval tokens are immutable, and thus aren't really copied. 595 */ clone()596 public Object clone() { 597 CalculatorExpr result = new CalculatorExpr(); 598 for (Token t: mExpr) { 599 if (t instanceof Constant) { 600 result.mExpr.add((Token)(((Constant)t).clone())); 601 } else { 602 result.mExpr.add(t); 603 } 604 } 605 return result; 606 } 607 608 // Am I just a constant? isConstant()609 public boolean isConstant() { 610 if (mExpr.size() != 1) { 611 return false; 612 } 613 return mExpr.get(0) instanceof Constant; 614 } 615 616 /** 617 * Return a new expression consisting of a single token representing the current pre-evaluated 618 * expression. 619 * The caller supplies the value, degree mode, and short string representation, which must 620 * have been previously computed. Thus this is guaranteed to terminate reasonably quickly. 621 */ abbreviate(CR val, BoundedRational ratVal, boolean dm, String sr)622 public CalculatorExpr abbreviate(CR val, BoundedRational ratVal, 623 boolean dm, String sr) { 624 CalculatorExpr result = new CalculatorExpr(); 625 Token t = new PreEval(val, ratVal, new CalculatorExpr((ArrayList<Token>) mExpr.clone()), 626 new EvalContext(dm, mExpr.size()), sr); 627 result.mExpr.add(t); 628 return result; 629 } 630 631 /** 632 * Internal evaluation functions return an EvalRet triple. 633 * We compute rational (BoundedRational) results when possible, both as a performance 634 * optimization, and to detect errors exactly when we can. 635 */ 636 private static class EvalRet { 637 public int pos; // Next position (expression index) to be parsed. 638 public final CR val; // Constructive Real result of evaluating subexpression. 639 public final BoundedRational ratVal; // Exact Rational value or null. EvalRet(int p, CR v, BoundedRational r)640 EvalRet(int p, CR v, BoundedRational r) { 641 pos = p; 642 val = v; 643 ratVal = r; 644 } 645 } 646 647 /** 648 * Internal evaluation functions take an EvalContext argument. 649 */ 650 private static class EvalContext { 651 public final int mPrefixLength; // Length of prefix to evaluate. Not explicitly saved. 652 public final boolean mDegreeMode; 653 // If we add any other kinds of evaluation modes, they go here. EvalContext(boolean degreeMode, int len)654 EvalContext(boolean degreeMode, int len) { 655 mDegreeMode = degreeMode; 656 mPrefixLength = len; 657 } EvalContext(DataInput in, int len)658 EvalContext(DataInput in, int len) throws IOException { 659 mDegreeMode = in.readBoolean(); 660 mPrefixLength = len; 661 } write(DataOutput out)662 void write(DataOutput out) throws IOException { 663 out.writeBoolean(mDegreeMode); 664 } 665 } 666 667 private final CR RADIANS_PER_DEGREE = CR.PI.divide(CR.valueOf(180)); 668 669 private final CR DEGREES_PER_RADIAN = CR.valueOf(180).divide(CR.PI); 670 toRadians(CR x, EvalContext ec)671 private CR toRadians(CR x, EvalContext ec) { 672 if (ec.mDegreeMode) { 673 return x.multiply(RADIANS_PER_DEGREE); 674 } else { 675 return x; 676 } 677 } 678 fromRadians(CR x, EvalContext ec)679 private CR fromRadians(CR x, EvalContext ec) { 680 if (ec.mDegreeMode) { 681 return x.multiply(DEGREES_PER_RADIAN); 682 } else { 683 return x; 684 } 685 } 686 687 // The following methods can all throw IndexOutOfBoundsException in the event of a syntax 688 // error. We expect that to be caught in eval below. 689 isOperatorUnchecked(int i, int op)690 private boolean isOperatorUnchecked(int i, int op) { 691 Token t = mExpr.get(i); 692 if (!(t instanceof Operator)) { 693 return false; 694 } 695 return ((Operator)(t)).id == op; 696 } 697 isOperator(int i, int op, EvalContext ec)698 private boolean isOperator(int i, int op, EvalContext ec) { 699 if (i >= ec.mPrefixLength) { 700 return false; 701 } 702 return isOperatorUnchecked(i, op); 703 } 704 705 public static class SyntaxException extends Exception { SyntaxException()706 public SyntaxException() { 707 super(); 708 } SyntaxException(String s)709 public SyntaxException(String s) { 710 super(s); 711 } 712 } 713 714 // The following functions all evaluate some kind of expression starting at position i in 715 // mExpr in a specified evaluation context. They return both the expression value (as 716 // constructive real and, if applicable, as BoundedRational) and the position of the next token 717 // that was not used as part of the evaluation. 718 // This is essentially a simple recursive descent parser combined with expression evaluation. 719 evalUnary(int i, EvalContext ec)720 private EvalRet evalUnary(int i, EvalContext ec) throws SyntaxException { 721 final Token t = mExpr.get(i); 722 BoundedRational ratVal; 723 if (t instanceof Constant) { 724 Constant c = (Constant)t; 725 ratVal = c.toRational(); 726 return new EvalRet(i+1, ratVal.CRValue(), ratVal); 727 } 728 if (t instanceof PreEval) { 729 final PreEval p = (PreEval)t; 730 return new EvalRet(i+1, p.value, p.ratValue); 731 } 732 EvalRet argVal; 733 switch(((Operator)(t)).id) { 734 case R.id.const_pi: 735 return new EvalRet(i+1, CR.PI, null); 736 case R.id.const_e: 737 return new EvalRet(i+1, REAL_E, null); 738 case R.id.op_sqrt: 739 // Seems to have highest precedence. 740 // Does not add implicit paren. 741 // Does seem to accept a leading minus. 742 if (isOperator(i+1, R.id.op_sub, ec)) { 743 argVal = evalUnary(i+2, ec); 744 ratVal = BoundedRational.sqrt(BoundedRational.negate(argVal.ratVal)); 745 if (ratVal != null) { 746 break; 747 } 748 return new EvalRet(argVal.pos, 749 argVal.val.negate().sqrt(), null); 750 } else { 751 argVal = evalUnary(i+1, ec); 752 ratVal = BoundedRational.sqrt(argVal.ratVal); 753 if (ratVal != null) { 754 break; 755 } 756 return new EvalRet(argVal.pos, argVal.val.sqrt(), null); 757 } 758 case R.id.lparen: 759 argVal = evalExpr(i+1, ec); 760 if (isOperator(argVal.pos, R.id.rparen, ec)) { 761 argVal.pos++; 762 } 763 return new EvalRet(argVal.pos, argVal.val, argVal.ratVal); 764 case R.id.fun_sin: 765 argVal = evalExpr(i+1, ec); 766 if (isOperator(argVal.pos, R.id.rparen, ec)) { 767 argVal.pos++; 768 } 769 ratVal = ec.mDegreeMode ? BoundedRational.degreeSin(argVal.ratVal) 770 : BoundedRational.sin(argVal.ratVal); 771 if (ratVal != null) { 772 break; 773 } 774 return new EvalRet(argVal.pos, toRadians(argVal.val,ec).sin(), null); 775 case R.id.fun_cos: 776 argVal = evalExpr(i+1, ec); 777 if (isOperator(argVal.pos, R.id.rparen, ec)) { 778 argVal.pos++; 779 } 780 ratVal = ec.mDegreeMode ? BoundedRational.degreeCos(argVal.ratVal) 781 : BoundedRational.cos(argVal.ratVal); 782 if (ratVal != null) { 783 break; 784 } 785 return new EvalRet(argVal.pos, toRadians(argVal.val,ec).cos(), null); 786 case R.id.fun_tan: 787 argVal = evalExpr(i+1, ec); 788 if (isOperator(argVal.pos, R.id.rparen, ec)) { 789 argVal.pos++; 790 } 791 ratVal = ec.mDegreeMode ? BoundedRational.degreeTan(argVal.ratVal) 792 : BoundedRational.tan(argVal.ratVal); 793 if (ratVal != null) { 794 break; 795 } 796 CR argCR = toRadians(argVal.val, ec); 797 return new EvalRet(argVal.pos, argCR.sin().divide(argCR.cos()), null); 798 case R.id.fun_ln: 799 argVal = evalExpr(i+1, ec); 800 if (isOperator(argVal.pos, R.id.rparen, ec)) { 801 argVal.pos++; 802 } 803 ratVal = BoundedRational.ln(argVal.ratVal); 804 if (ratVal != null) { 805 break; 806 } 807 return new EvalRet(argVal.pos, argVal.val.ln(), null); 808 case R.id.fun_exp: 809 argVal = evalExpr(i+1, ec); 810 if (isOperator(argVal.pos, R.id.rparen, ec)) { 811 argVal.pos++; 812 } 813 ratVal = BoundedRational.exp(argVal.ratVal); 814 if (ratVal != null) { 815 break; 816 } 817 return new EvalRet(argVal.pos, argVal.val.exp(), null); 818 case R.id.fun_log: 819 argVal = evalExpr(i+1, ec); 820 if (isOperator(argVal.pos, R.id.rparen, ec)) { 821 argVal.pos++; 822 } 823 ratVal = BoundedRational.log(argVal.ratVal); 824 if (ratVal != null) { 825 break; 826 } 827 return new EvalRet(argVal.pos, argVal.val.ln().divide(CR.valueOf(10).ln()), null); 828 case R.id.fun_arcsin: 829 argVal = evalExpr(i+1, ec); 830 if (isOperator(argVal.pos, R.id.rparen, ec)) { 831 argVal.pos++; 832 } 833 ratVal = ec.mDegreeMode ? BoundedRational.degreeAsin(argVal.ratVal) 834 : BoundedRational.asin(argVal.ratVal); 835 if (ratVal != null) { 836 break; 837 } 838 return new EvalRet(argVal.pos, 839 fromRadians(UnaryCRFunction.asinFunction.execute(argVal.val),ec), null); 840 case R.id.fun_arccos: 841 argVal = evalExpr(i+1, ec); 842 if (isOperator(argVal.pos, R.id.rparen, ec)) { 843 argVal.pos++; 844 } 845 ratVal = ec.mDegreeMode ? BoundedRational.degreeAcos(argVal.ratVal) 846 : BoundedRational.acos(argVal.ratVal); 847 if (ratVal != null) { 848 break; 849 } 850 return new EvalRet(argVal.pos, 851 fromRadians(UnaryCRFunction.acosFunction.execute(argVal.val),ec), null); 852 case R.id.fun_arctan: 853 argVal = evalExpr(i+1, ec); 854 if (isOperator(argVal.pos, R.id.rparen, ec)) { 855 argVal.pos++; 856 } 857 ratVal = ec.mDegreeMode ? BoundedRational.degreeAtan(argVal.ratVal) 858 : BoundedRational.atan(argVal.ratVal); 859 if (ratVal != null) { 860 break; 861 } 862 return new EvalRet(argVal.pos, 863 fromRadians(UnaryCRFunction.atanFunction.execute(argVal.val),ec), null); 864 default: 865 throw new SyntaxException("Unrecognized token in expression"); 866 } 867 // We have a rational value. 868 return new EvalRet(argVal.pos, ratVal.CRValue(), ratVal); 869 } 870 871 /** 872 * Compute an integral power of a constructive real. 873 * Unlike the "general" case using logarithms, this handles a negative base. 874 */ pow(CR base, BigInteger exp)875 private static CR pow(CR base, BigInteger exp) { 876 if (exp.compareTo(BigInteger.ZERO) < 0) { 877 return pow(base, exp.negate()).inverse(); 878 } 879 if (exp.equals(BigInteger.ONE)) { 880 return base; 881 } 882 if (exp.and(BigInteger.ONE).intValue() == 1) { 883 return pow(base, exp.subtract(BigInteger.ONE)).multiply(base); 884 } 885 if (exp.equals(BigInteger.ZERO)) { 886 return CR.valueOf(1); 887 } 888 CR tmp = pow(base, exp.shiftRight(1)); 889 return tmp.multiply(tmp); 890 } 891 892 // Number of bits past binary point to test for integer-ness. 893 private static final int TEST_PREC = -100; 894 private static final BigInteger MASK = 895 BigInteger.ONE.shiftLeft(-TEST_PREC).subtract(BigInteger.ONE); 896 private static final CR REAL_E = CR.valueOf(1).exp(); 897 private static final CR REAL_ONE_HUNDREDTH = CR.valueOf(100).inverse(); 898 private static final BoundedRational RATIONAL_ONE_HUNDREDTH = new BoundedRational(1,100); isApprInt(CR x)899 private static boolean isApprInt(CR x) { 900 BigInteger appr = x.get_appr(TEST_PREC); 901 return appr.and(MASK).signum() == 0; 902 } 903 evalSuffix(int i, EvalContext ec)904 private EvalRet evalSuffix(int i, EvalContext ec) throws SyntaxException { 905 final EvalRet tmp = evalUnary(i, ec); 906 int cpos = tmp.pos; 907 CR crVal = tmp.val; 908 BoundedRational ratVal = tmp.ratVal; 909 boolean isFact; 910 boolean isSquared = false; 911 while ((isFact = isOperator(cpos, R.id.op_fact, ec)) || 912 (isSquared = isOperator(cpos, R.id.op_sqr, ec)) || 913 isOperator(cpos, R.id.op_pct, ec)) { 914 if (isFact) { 915 if (ratVal == null) { 916 // Assume it was an integer, but we didn't figure it out. 917 // KitKat may have used the Gamma function. 918 if (!isApprInt(crVal)) { 919 throw new ArithmeticException("factorial(non-integer)"); 920 } 921 ratVal = new BoundedRational(crVal.BigIntegerValue()); 922 } 923 ratVal = BoundedRational.fact(ratVal); 924 crVal = ratVal.CRValue(); 925 } else if (isSquared) { 926 ratVal = BoundedRational.multiply(ratVal, ratVal); 927 if (ratVal == null) { 928 crVal = crVal.multiply(crVal); 929 } else { 930 crVal = ratVal.CRValue(); 931 } 932 } else /* percent */ { 933 ratVal = BoundedRational.multiply(ratVal, RATIONAL_ONE_HUNDREDTH); 934 if (ratVal == null) { 935 crVal = crVal.multiply(REAL_ONE_HUNDREDTH); 936 } else { 937 crVal = ratVal.CRValue(); 938 } 939 } 940 ++cpos; 941 } 942 return new EvalRet(cpos, crVal, ratVal); 943 } 944 evalFactor(int i, EvalContext ec)945 private EvalRet evalFactor(int i, EvalContext ec) throws SyntaxException { 946 final EvalRet result1 = evalSuffix(i, ec); 947 int cpos = result1.pos; // current position 948 CR crVal = result1.val; // value so far 949 BoundedRational ratVal = result1.ratVal; // int value so far 950 if (isOperator(cpos, R.id.op_pow, ec)) { 951 final EvalRet exp = evalSignedFactor(cpos + 1, ec); 952 cpos = exp.pos; 953 // Try completely rational evaluation first. 954 ratVal = BoundedRational.pow(ratVal, exp.ratVal); 955 if (ratVal != null) { 956 return new EvalRet(cpos, ratVal.CRValue(), ratVal); 957 } 958 // Power with integer exponent is defined for negative base. 959 // Thus we handle that case separately. 960 // We punt if the exponent is an integer computed from irrational 961 // values. That wouldn't work reliably with floating point either. 962 BigInteger int_exp = BoundedRational.asBigInteger(exp.ratVal); 963 if (int_exp != null) { 964 crVal = pow(crVal, int_exp); 965 } else { 966 crVal = crVal.ln().multiply(exp.val).exp(); 967 } 968 ratVal = null; 969 } 970 return new EvalRet(cpos, crVal, ratVal); 971 } 972 evalSignedFactor(int i, EvalContext ec)973 private EvalRet evalSignedFactor(int i, EvalContext ec) throws SyntaxException { 974 final boolean negative = isOperator(i, R.id.op_sub, ec); 975 int cpos = negative ? i + 1 : i; 976 EvalRet tmp = evalFactor(cpos, ec); 977 cpos = tmp.pos; 978 CR crVal = negative ? tmp.val.negate() : tmp.val; 979 BoundedRational ratVal = negative ? BoundedRational.negate(tmp.ratVal) 980 : tmp.ratVal; 981 return new EvalRet(cpos, crVal, ratVal); 982 } 983 canStartFactor(int i)984 private boolean canStartFactor(int i) { 985 if (i >= mExpr.size()) return false; 986 Token t = mExpr.get(i); 987 if (!(t instanceof Operator)) return true; 988 int id = ((Operator)(t)).id; 989 if (KeyMaps.isBinary(id)) return false; 990 switch (id) { 991 case R.id.op_fact: 992 case R.id.rparen: 993 return false; 994 default: 995 return true; 996 } 997 } 998 evalTerm(int i, EvalContext ec)999 private EvalRet evalTerm(int i, EvalContext ec) throws SyntaxException { 1000 EvalRet tmp = evalSignedFactor(i, ec); 1001 boolean is_mul = false; 1002 boolean is_div = false; 1003 int cpos = tmp.pos; // Current position in expression. 1004 CR crVal = tmp.val; // Current value. 1005 BoundedRational ratVal = tmp.ratVal; // Current rational value. 1006 while ((is_mul = isOperator(cpos, R.id.op_mul, ec)) 1007 || (is_div = isOperator(cpos, R.id.op_div, ec)) 1008 || canStartFactor(cpos)) { 1009 if (is_mul || is_div) ++cpos; 1010 tmp = evalSignedFactor(cpos, ec); 1011 if (is_div) { 1012 ratVal = BoundedRational.divide(ratVal, tmp.ratVal); 1013 if (ratVal == null) { 1014 crVal = crVal.divide(tmp.val); 1015 } else { 1016 crVal = ratVal.CRValue(); 1017 } 1018 } else { 1019 ratVal = BoundedRational.multiply(ratVal, tmp.ratVal); 1020 if (ratVal == null) { 1021 crVal = crVal.multiply(tmp.val); 1022 } else { 1023 crVal = ratVal.CRValue(); 1024 } 1025 } 1026 cpos = tmp.pos; 1027 is_mul = is_div = false; 1028 } 1029 return new EvalRet(cpos, crVal, ratVal); 1030 } 1031 1032 /** 1033 * Is the subexpression starting at pos a simple percent constant? 1034 * This is used to recognize exppressions like 200+10%, which we handle specially. 1035 * This is defined as a Constant or PreEval token, followed by a percent sign, and followed 1036 * by either nothing or an additive operator. 1037 * Note that we are intentionally far more restrictive in recognizing such expressions than 1038 * e.g. http://blogs.msdn.com/b/oldnewthing/archive/2008/01/10/7047497.aspx . 1039 * When in doubt, we fall back to the the naive interpretation of % as 1/100. 1040 * Note that 100+(10)% yields 100.1 while 100+10% yields 110. This may be controversial, 1041 * but is consistent with Google web search. 1042 */ isPercent(int pos)1043 private boolean isPercent(int pos) { 1044 if (mExpr.size() < pos + 2 || !isOperatorUnchecked(pos + 1, R.id.op_pct)) { 1045 return false; 1046 } 1047 Token number = mExpr.get(pos); 1048 if (number instanceof Operator) { 1049 return false; 1050 } 1051 if (mExpr.size() == pos + 2) { 1052 return true; 1053 } 1054 if (!(mExpr.get(pos + 2) instanceof Operator)) { 1055 return false; 1056 } 1057 Operator op = (Operator) mExpr.get(pos + 2); 1058 return op.id == R.id.op_add || op.id == R.id.op_sub; 1059 } 1060 1061 /** 1062 * Compute the multiplicative factor corresponding to an N% addition or subtraction. 1063 * @param pos position of Constant or PreEval expression token corresponding to N 1064 * @param isSubtraction this is a subtraction, as opposed to addition 1065 * @param ec usable evaluation contex; only length matters 1066 * @return Rational and CR values; position is pos + 2, i.e. after percent sign 1067 */ getPercentFactor(int pos, boolean isSubtraction, EvalContext ec)1068 private EvalRet getPercentFactor(int pos, boolean isSubtraction, EvalContext ec) 1069 throws SyntaxException { 1070 EvalRet tmp = evalUnary(pos, ec); 1071 BoundedRational ratVal = isSubtraction ? BoundedRational.negate(tmp.ratVal) 1072 : tmp.ratVal; 1073 CR crVal = isSubtraction ? tmp.val.negate() : tmp.val; 1074 ratVal = BoundedRational.add(BoundedRational.ONE, 1075 BoundedRational.multiply(ratVal, RATIONAL_ONE_HUNDREDTH)); 1076 if (ratVal == null) { 1077 crVal = CR.ONE.add(crVal.multiply(REAL_ONE_HUNDREDTH)); 1078 } else { 1079 crVal = ratVal.CRValue(); 1080 } 1081 return new EvalRet(pos + 2 /* after percent sign */, crVal, ratVal); 1082 } 1083 evalExpr(int i, EvalContext ec)1084 private EvalRet evalExpr(int i, EvalContext ec) throws SyntaxException { 1085 EvalRet tmp = evalTerm(i, ec); 1086 boolean is_plus; 1087 int cpos = tmp.pos; 1088 CR crVal = tmp.val; 1089 BoundedRational ratVal = tmp.ratVal; 1090 while ((is_plus = isOperator(cpos, R.id.op_add, ec)) 1091 || isOperator(cpos, R.id.op_sub, ec)) { 1092 if (isPercent(cpos + 1)) { 1093 tmp = getPercentFactor(cpos + 1, !is_plus, ec); 1094 ratVal = BoundedRational.multiply(ratVal, tmp.ratVal); 1095 if (ratVal == null) { 1096 crVal = crVal.multiply(tmp.val); 1097 } else { 1098 crVal = ratVal.CRValue(); 1099 } 1100 } else { 1101 tmp = evalTerm(cpos + 1, ec); 1102 if (is_plus) { 1103 ratVal = BoundedRational.add(ratVal, tmp.ratVal); 1104 if (ratVal == null) { 1105 crVal = crVal.add(tmp.val); 1106 } else { 1107 crVal = ratVal.CRValue(); 1108 } 1109 } else { 1110 ratVal = BoundedRational.subtract(ratVal, tmp.ratVal); 1111 if (ratVal == null) { 1112 crVal = crVal.subtract(tmp.val); 1113 } else { 1114 crVal = ratVal.CRValue(); 1115 } 1116 } 1117 } 1118 cpos = tmp.pos; 1119 } 1120 return new EvalRet(cpos, crVal, ratVal); 1121 } 1122 1123 /** 1124 * Externally visible evaluation result. 1125 */ 1126 public static class EvalResult { 1127 public final CR val; 1128 public final BoundedRational ratVal; EvalResult(CR v, BoundedRational rv)1129 EvalResult (CR v, BoundedRational rv) { 1130 val = v; 1131 ratVal = rv; 1132 } 1133 } 1134 1135 /** 1136 * Return the starting position of the sequence of trailing binary operators. 1137 */ trailingBinaryOpsStart()1138 private int trailingBinaryOpsStart() { 1139 int result = mExpr.size(); 1140 while (result > 0) { 1141 Token last = mExpr.get(result - 1); 1142 if (!(last instanceof Operator)) break; 1143 Operator o = (Operator)last; 1144 if (!KeyMaps.isBinary(o.id)) break; 1145 --result; 1146 } 1147 return result; 1148 } 1149 1150 /** 1151 * Is the current expression worth evaluating? 1152 */ hasInterestingOps()1153 public boolean hasInterestingOps() { 1154 int last = trailingBinaryOpsStart(); 1155 int first = 0; 1156 if (last > first && isOperatorUnchecked(first, R.id.op_sub)) { 1157 // Leading minus is not by itself interesting. 1158 first++; 1159 } 1160 for (int i = first; i < last; ++i) { 1161 Token t1 = mExpr.get(i); 1162 if (t1 instanceof Operator 1163 || t1 instanceof PreEval && ((PreEval)t1).hasEllipsis()) { 1164 return true; 1165 } 1166 } 1167 return false; 1168 } 1169 1170 /** 1171 * Evaluate the expression excluding trailing binary operators. 1172 * Errors result in exceptions, most of which are unchecked. Should not be called 1173 * concurrently with modification of the expression. May take a very long time; avoid calling 1174 * from UI thread. 1175 * 1176 * @param degreeMode use degrees rather than radians 1177 */ eval(boolean degreeMode)1178 EvalResult eval(boolean degreeMode) throws SyntaxException 1179 // And unchecked exceptions thrown by CR 1180 // and BoundedRational. 1181 { 1182 try { 1183 // We currently never include trailing binary operators, but include other trailing 1184 // operators. Thus we usually, but not always, display results for prefixes of valid 1185 // expressions, and don't generate an error where we previously displayed an instant 1186 // result. This reflects the Android L design. 1187 int prefixLen = trailingBinaryOpsStart(); 1188 EvalContext ec = new EvalContext(degreeMode, prefixLen); 1189 EvalRet res = evalExpr(0, ec); 1190 if (res.pos != prefixLen) { 1191 throw new SyntaxException("Failed to parse full expression"); 1192 } 1193 return new EvalResult(res.val, res.ratVal); 1194 } catch (IndexOutOfBoundsException e) { 1195 throw new SyntaxException("Unexpected expression end"); 1196 } 1197 } 1198 1199 // Produce a string representation of the expression itself toSpannableStringBuilder(Context context)1200 SpannableStringBuilder toSpannableStringBuilder(Context context) { 1201 SpannableStringBuilder ssb = new SpannableStringBuilder(); 1202 for (Token t: mExpr) { 1203 ssb.append(t.toCharSequence(context)); 1204 } 1205 return ssb; 1206 } 1207 } 1208