blob: 8b3cb408ca41c76856c358ee3770832fc1102859 [file] [log] [blame]
Hans Boehm84614952014-11-25 18:46:17 -08001/*
Hans Boehme4579122015-05-26 17:52:32 -07002 * Copyright (C) 2015 The Android Open Source Project
Hans Boehm84614952014-11-25 18:46:17 -08003 *
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
17package com.android.calculator2;
18
Hans Boehm84614952014-11-25 18:46:17 -080019
20import com.hp.creals.CR;
21import com.hp.creals.UnaryCRFunction;
Hans Boehm84614952014-11-25 18:46:17 -080022
23import android.content.Context;
Hans Boehmc023b732015-04-29 11:30:47 -070024import android.util.Log;
Hans Boehm84614952014-11-25 18:46:17 -080025
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070026import java.math.BigInteger;
27import java.io.DataInput;
28import java.io.DataOutput;
29import java.io.IOException;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070030import java.util.ArrayList;
31import java.util.HashMap;
32import java.util.IdentityHashMap;
33
Hans Boehm84614952014-11-25 18:46:17 -080034// A mathematical expression represented as a sequence of "tokens".
35// Many tokes are represented by button ids for the corresponding operator.
36// Parsed only when we evaluate the expression using the "eval" method.
37class CalculatorExpr {
38 private ArrayList<Token> mExpr; // The actual representation
39 // as a list of tokens. Constant
40 // tokens are always nonempty.
41
42 private static enum TokenKind { CONSTANT, OPERATOR, PRE_EVAL };
43 private static TokenKind[] tokenKindValues = TokenKind.values();
44 private final static BigInteger BIG_MILLION = BigInteger.valueOf(1000000);
45 private final static BigInteger BIG_BILLION = BigInteger.valueOf(1000000000);
46
47 private static abstract class Token {
48 abstract TokenKind kind();
49 abstract void write(DataOutput out) throws IOException;
50 // Implementation writes kind as Byte followed by
51 // data read by constructor.
52 abstract String toString(Context context);
53 // We need the context to convert button ids to strings.
54 }
55
56 // An operator token
57 private static class Operator extends Token {
58 final int mId; // We use the button resource id
59 Operator(int resId) {
60 mId = resId;
61 }
62 Operator(DataInput in) throws IOException {
63 mId = in.readInt();
64 }
65 @Override
66 void write(DataOutput out) throws IOException {
67 out.writeByte(TokenKind.OPERATOR.ordinal());
68 out.writeInt(mId);
69 }
70 @Override
71 public String toString(Context context) {
Justin Klaassene2711cb2015-05-28 11:13:17 -070072 return KeyMaps.toString(context, mId);
Hans Boehm84614952014-11-25 18:46:17 -080073 }
74 @Override
75 TokenKind kind() { return TokenKind.OPERATOR; }
76 }
77
Hans Boehm682ff5e2015-03-09 14:40:25 -070078 // A (possibly incomplete) numerical constant.
79 // Supports addition and removal of trailing characters; hence mutable.
Hans Boehm84614952014-11-25 18:46:17 -080080 private static class Constant extends Token implements Cloneable {
81 private boolean mSawDecimal;
82 String mWhole; // part before decimal point
83 private String mFraction; // part after decimal point
84
85 Constant() {
86 mWhole = "";
87 mFraction = "";
88 mSawDecimal = false;
89 };
90
91 Constant(DataInput in) throws IOException {
92 mWhole = in.readUTF();
93 mSawDecimal = in.readBoolean();
94 mFraction = in.readUTF();
95 }
96
97 @Override
98 void write(DataOutput out) throws IOException {
99 out.writeByte(TokenKind.CONSTANT.ordinal());
100 out.writeUTF(mWhole);
101 out.writeBoolean(mSawDecimal);
102 out.writeUTF(mFraction);
103 }
104
105 // Given a button press, append corresponding digit.
106 // We assume id is a digit or decimal point.
107 // Just return false if this was the second (or later) decimal point
108 // in this constant.
109 boolean add(int id) {
110 if (id == R.id.dec_point) {
111 if (mSawDecimal) return false;
112 mSawDecimal = true;
113 return true;
114 }
115 int val = KeyMaps.digVal(id);
116 if (mSawDecimal) {
117 mFraction += val;
118 } else {
119 mWhole += val;
120 }
121 return true;
122 }
123
124 // Undo the last add.
125 // Assumes the constant is nonempty.
126 void delete() {
127 if (!mFraction.isEmpty()) {
128 mFraction = mFraction.substring(0, mFraction.length() - 1);
129 } else if (mSawDecimal) {
130 mSawDecimal = false;
131 } else {
132 mWhole = mWhole.substring(0, mWhole.length() - 1);
133 }
134 }
135
136 boolean isEmpty() {
137 return (mSawDecimal == false && mWhole.isEmpty());
138 }
139
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700140 // Produces human-readable string, as typed.
Hans Boehm013969e2015-04-13 20:29:47 -0700141 // Result is internationalized.
Hans Boehm84614952014-11-25 18:46:17 -0800142 @Override
143 public String toString() {
144 String result = mWhole;
145 if (mSawDecimal) {
Hans Boehm013969e2015-04-13 20:29:47 -0700146 result += '.';
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700147 result += mFraction;
148 }
Hans Boehm013969e2015-04-13 20:29:47 -0700149 return KeyMaps.translateResult(result);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700150 }
151
152 // Eliminates leading decimal, which some of our
153 // other packages don't like.
Hans Boehm013969e2015-04-13 20:29:47 -0700154 // Meant for machine consumption:
155 // Doesn't internationalize decimal point or digits.
156 public String toEasyString() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700157 String result = mWhole;
158 if (result.isEmpty()) result = "0";
159 if (mSawDecimal) {
Hans Boehm84614952014-11-25 18:46:17 -0800160 result += '.';
161 result += mFraction;
162 }
163 return result;
164 }
165
Hans Boehm682ff5e2015-03-09 14:40:25 -0700166 public BoundedRational toRational() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700167 String whole = mWhole;
168 if (whole.isEmpty()) whole = "0";
169 BigInteger num = new BigInteger(whole + mFraction);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700170 BigInteger den = BigInteger.TEN.pow(mFraction.length());
171 return new BoundedRational(num, den);
172 }
173
Hans Boehm84614952014-11-25 18:46:17 -0800174 @Override
175 String toString(Context context) {
176 return toString();
177 }
178
179 @Override
180 TokenKind kind() { return TokenKind.CONSTANT; }
181
182 // Override clone to make it public
183 @Override
184 public Object clone() {
185 Constant res = new Constant();
186 res.mWhole = mWhole;
187 res.mFraction = mFraction;
188 res.mSawDecimal = mSawDecimal;
189 return res;
190 }
191 }
192
193 // Hash maps used to detect duplicate subexpressions when
194 // we write out CalculatorExprs and read them back in.
195 private static final ThreadLocal<IdentityHashMap<CR,Integer>>outMap =
196 new ThreadLocal<IdentityHashMap<CR,Integer>>();
197 // Maps expressions to indices on output
198 private static final ThreadLocal<HashMap<Integer,PreEval>>inMap =
199 new ThreadLocal<HashMap<Integer,PreEval>>();
200 // Maps expressions to indices on output
201 private static final ThreadLocal<Integer> exprIndex =
202 new ThreadLocal<Integer>();
203
204 static void initExprOutput() {
205 outMap.set(new IdentityHashMap<CR,Integer>());
206 exprIndex.set(Integer.valueOf(0));
207 }
208
209 static void initExprInput() {
210 inMap.set(new HashMap<Integer,PreEval>());
211 }
212
213 // We treat previously evaluated subexpressions as tokens
214 // These are inserted when either:
215 // - We continue an expression after evaluating some of it.
216 // - TODO: When we copy/paste expressions.
217 // The representation includes three different representations
218 // of the expression:
219 // 1) The CR value for use in computation.
220 // 2) The integer value for use in the computations,
221 // if the expression evaluates to an integer.
222 // 3a) The corresponding CalculatorExpr, together with
223 // 3b) The context (currently just deg/rad mode) used to evaluate
224 // the expression.
225 // 4) A short string representation that is used to
226 // Display the expression.
227 //
228 // (3) is present only so that we can persist the object.
229 // (4) is stored explicitly to avoid waiting for recomputation in the UI
230 // thread.
231 private static class PreEval extends Token {
232 final CR mValue;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700233 final BoundedRational mRatValue;
Hans Boehm84614952014-11-25 18:46:17 -0800234 private final CalculatorExpr mExpr;
235 private final EvalContext mContext;
Hans Boehm50ed3202015-06-09 14:35:49 -0700236 private final String mShortRep; // Not internationalized.
Hans Boehm013969e2015-04-13 20:29:47 -0700237 PreEval(CR val, BoundedRational ratVal, CalculatorExpr expr,
238 EvalContext ec, String shortRep) {
Hans Boehm84614952014-11-25 18:46:17 -0800239 mValue = val;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700240 mRatValue = ratVal;
Hans Boehm84614952014-11-25 18:46:17 -0800241 mExpr = expr;
242 mContext = ec;
243 mShortRep = shortRep;
244 }
245 // In writing out PreEvals, we are careful to avoid writing
246 // out duplicates. We assume that two expressions are
247 // duplicates if they have the same mVal. This avoids a
248 // potential exponential blow up in certain off cases and
249 // redundant evaluation after reading them back in.
250 // The parameter hash map maps expressions we've seen
251 // before to their index.
252 @Override
253 void write(DataOutput out) throws IOException {
254 out.writeByte(TokenKind.PRE_EVAL.ordinal());
255 Integer index = outMap.get().get(mValue);
256 if (index == null) {
257 int nextIndex = exprIndex.get() + 1;
258 exprIndex.set(nextIndex);
259 outMap.get().put(mValue, nextIndex);
260 out.writeInt(nextIndex);
261 mExpr.write(out);
262 mContext.write(out);
263 out.writeUTF(mShortRep);
264 } else {
265 // Just write out the index
266 out.writeInt(index);
267 }
268 }
269 PreEval(DataInput in) throws IOException {
270 int index = in.readInt();
271 PreEval prev = inMap.get().get(index);
272 if (prev == null) {
273 mExpr = new CalculatorExpr(in);
Hans Boehmc023b732015-04-29 11:30:47 -0700274 mContext = new EvalContext(in, mExpr.mExpr.size());
Hans Boehm84614952014-11-25 18:46:17 -0800275 // Recompute other fields
276 // We currently do this in the UI thread, but we
277 // only create PreEval expressions that were
278 // previously successfully evaluated, and thus
279 // don't diverge. We also only evaluate to a
280 // constructive real, which involves substantial
281 // work only in fairly contrived circumstances.
282 // TODO: Deal better with slow evaluations.
Hans Boehmc023b732015-04-29 11:30:47 -0700283 EvalRet res = null;
284 try {
285 res = mExpr.evalExpr(0, mContext);
286 } catch (SyntaxException e) {
287 // Should be impossible, since we only write out
288 // expressions that can be evaluated.
289 Log.e("Calculator", "Unexpected syntax exception" + e);
290 }
Hans Boehm84614952014-11-25 18:46:17 -0800291 mValue = res.mVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700292 mRatValue = res.mRatVal;
Hans Boehm84614952014-11-25 18:46:17 -0800293 mShortRep = in.readUTF();
294 inMap.get().put(index, this);
295 } else {
296 mValue = prev.mValue;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700297 mRatValue = prev.mRatValue;
Hans Boehm84614952014-11-25 18:46:17 -0800298 mExpr = prev.mExpr;
299 mContext = prev.mContext;
300 mShortRep = prev.mShortRep;
301 }
302 }
303 @Override
304 String toString(Context context) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700305 return KeyMaps.translateResult(mShortRep);
Hans Boehm84614952014-11-25 18:46:17 -0800306 }
307 @Override
Hans Boehm187d3e92015-06-09 18:04:26 -0700308 TokenKind kind() {
309 return TokenKind.PRE_EVAL;
310 }
311 boolean hasEllipsis() {
312 return mShortRep.lastIndexOf(KeyMaps.ELLIPSIS) != -1;
313 }
Hans Boehm84614952014-11-25 18:46:17 -0800314 }
315
316 static Token newToken(DataInput in) throws IOException {
317 TokenKind kind = tokenKindValues[in.readByte()];
318 switch(kind) {
319 case CONSTANT:
320 return new Constant(in);
321 case OPERATOR:
322 return new Operator(in);
323 case PRE_EVAL:
324 return new PreEval(in);
325 default: throw new IOException("Bad save file format");
326 }
327 }
328
329 CalculatorExpr() {
330 mExpr = new ArrayList<Token>();
331 }
332
333 private CalculatorExpr(ArrayList<Token> expr) {
334 mExpr = expr;
335 }
336
337 CalculatorExpr(DataInput in) throws IOException {
338 mExpr = new ArrayList<Token>();
339 int size = in.readInt();
340 for (int i = 0; i < size; ++i) {
341 mExpr.add(newToken(in));
342 }
343 }
344
345 void write(DataOutput out) throws IOException {
346 int size = mExpr.size();
347 out.writeInt(size);
348 for (int i = 0; i < size; ++i) {
349 mExpr.get(i).write(out);
350 }
351 }
352
353 private boolean hasTrailingBinary() {
354 int s = mExpr.size();
355 if (s == 0) return false;
356 Token t = mExpr.get(s-1);
357 if (!(t instanceof Operator)) return false;
358 Operator o = (Operator)t;
359 return (KeyMaps.isBinary(o.mId));
360 }
361
362 // Append press of button with given id to expression.
363 // Returns false and does nothing if this would clearly
364 // result in a syntax error.
365 boolean add(int id) {
366 int s = mExpr.size();
367 int d = KeyMaps.digVal(id);
368 boolean binary = KeyMaps.isBinary(id);
369 if (s == 0 && binary && id != R.id.op_sub) return false;
370 if (binary && hasTrailingBinary()
Hans Boehmc023b732015-04-29 11:30:47 -0700371 && (id != R.id.op_sub || isOperatorUnchecked(s-1, R.id.op_sub))) {
Hans Boehm84614952014-11-25 18:46:17 -0800372 return false;
373 }
374 boolean isConstPiece = (d != KeyMaps.NOT_DIGIT || id == R.id.dec_point);
375 if (isConstPiece) {
376 if (s == 0) {
377 mExpr.add(new Constant());
378 s++;
379 } else {
380 Token last = mExpr.get(s-1);
381 if(!(last instanceof Constant)) {
382 if (!(last instanceof Operator)) {
383 return false;
384 }
385 int lastOp = ((Operator)last).mId;
386 if (lastOp == R.id.const_e || lastOp == R.id.const_pi
387 || lastOp == R.id.op_fact
388 || lastOp == R.id.rparen) {
389 // Constant cannot possibly follow; reject immediately
390 return false;
391 }
392 mExpr.add(new Constant());
393 s++;
394 }
395 }
396 return ((Constant)(mExpr.get(s-1))).add(id);
397 } else {
398 mExpr.add(new Operator(id));
399 return true;
400 }
401 }
402
403 // Append the contents of the argument expression.
404 // It is assumed that the argument expression will not change,
405 // and thus its pieces can be reused directly.
406 // TODO: We probably only need this for expressions consisting of
407 // a single PreEval "token", and may want to check that.
408 void append(CalculatorExpr expr2) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700409 // Check that we're not concatenating Constant or PreEval
410 // tokens, since the result would look like a single constant
411 int s = mExpr.size();
Hans Boehm84614952014-11-25 18:46:17 -0800412 int s2 = expr2.mExpr.size();
Hans Boehmfbcef702015-04-27 18:07:47 -0700413 // Check that we're not concatenating Constant or PreEval
414 // tokens, since the result would look like a single constant,
415 // with very mysterious results for the user.
416 if (s != 0 && s2 != 0) {
417 Token last = mExpr.get(s-1);
418 Token first = expr2.mExpr.get(0);
419 if (!(first instanceof Operator) && !(last instanceof Operator)) {
420 // Fudge it by adding an explicit multiplication.
421 // We would have interpreted it as such anyway, and this
422 // makes it recognizable to the user.
423 mExpr.add(new Operator(R.id.op_mul));
424 }
425 }
Hans Boehm84614952014-11-25 18:46:17 -0800426 for (int i = 0; i < s2; ++i) {
427 mExpr.add(expr2.mExpr.get(i));
428 }
429 }
430
431 // Undo the last key addition, if any.
432 void delete() {
433 int s = mExpr.size();
434 if (s == 0) return;
435 Token last = mExpr.get(s-1);
436 if (last instanceof Constant) {
437 Constant c = (Constant)last;
438 c.delete();
439 if (!c.isEmpty()) return;
440 }
441 mExpr.remove(s-1);
442 }
443
444 void clear() {
445 mExpr.clear();
446 }
447
448 boolean isEmpty() {
449 return mExpr.isEmpty();
450 }
451
452 // Returns a logical deep copy of the CalculatorExpr.
453 // Operator and PreEval tokens are immutable, and thus
454 // aren't really copied.
455 public Object clone() {
456 CalculatorExpr res = new CalculatorExpr();
457 for (Token t: mExpr) {
458 if (t instanceof Constant) {
459 res.mExpr.add((Token)(((Constant)t).clone()));
460 } else {
461 res.mExpr.add(t);
462 }
463 }
464 return res;
465 }
466
467 // Am I just a constant?
468 boolean isConstant() {
469 if (mExpr.size() != 1) return false;
470 return mExpr.get(0) instanceof Constant;
471 }
472
473 // Return a new expression consisting of a single PreEval token
474 // representing the current expression.
475 // The caller supplies the value, degree mode, and short
476 // string representation, which must have been previously computed.
477 // Thus this is guaranteed to terminate reasonably quickly.
Hans Boehm682ff5e2015-03-09 14:40:25 -0700478 CalculatorExpr abbreviate(CR val, BoundedRational ratVal,
Hans Boehm84614952014-11-25 18:46:17 -0800479 boolean dm, String sr) {
480 CalculatorExpr result = new CalculatorExpr();
Hans Boehm682ff5e2015-03-09 14:40:25 -0700481 Token t = new PreEval(val, ratVal,
Hans Boehm84614952014-11-25 18:46:17 -0800482 new CalculatorExpr(
483 (ArrayList<Token>)mExpr.clone()),
Hans Boehmc023b732015-04-29 11:30:47 -0700484 new EvalContext(dm, mExpr.size()), sr);
Hans Boehm84614952014-11-25 18:46:17 -0800485 result.mExpr.add(t);
486 return result;
487 }
488
489 // Internal evaluation functions return an EvalRet triple.
Hans Boehm682ff5e2015-03-09 14:40:25 -0700490 // We compute rational (BoundedRational) results when possible, both as
Hans Boehm84614952014-11-25 18:46:17 -0800491 // a performance optimization, and to detect errors exactly when we can.
492 private class EvalRet {
493 int mPos; // Next position (expression index) to be parsed
494 final CR mVal; // Constructive Real result of evaluating subexpression
Hans Boehm682ff5e2015-03-09 14:40:25 -0700495 final BoundedRational mRatVal; // Exact Rational value or null if
496 // irrational or hard to compute.
497 EvalRet(int p, CR v, BoundedRational r) {
Hans Boehm84614952014-11-25 18:46:17 -0800498 mPos = p;
499 mVal = v;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700500 mRatVal = r;
Hans Boehm84614952014-11-25 18:46:17 -0800501 }
502 }
503
504 // And take a context argument:
505 private static class EvalContext {
Hans Boehmc023b732015-04-29 11:30:47 -0700506 public final int mPrefixLength; // Length of prefix to evaluate.
507 // Not explicitly saved.
508 public final boolean mDegreeMode;
Hans Boehm84614952014-11-25 18:46:17 -0800509 // If we add any other kinds of evaluation modes, they go here.
Hans Boehmc023b732015-04-29 11:30:47 -0700510 EvalContext(boolean degreeMode, int len) {
Hans Boehm84614952014-11-25 18:46:17 -0800511 mDegreeMode = degreeMode;
Hans Boehmc023b732015-04-29 11:30:47 -0700512 mPrefixLength = len;
Hans Boehm84614952014-11-25 18:46:17 -0800513 }
Hans Boehmc023b732015-04-29 11:30:47 -0700514 EvalContext(DataInput in, int len) throws IOException {
Hans Boehm84614952014-11-25 18:46:17 -0800515 mDegreeMode = in.readBoolean();
Hans Boehmc023b732015-04-29 11:30:47 -0700516 mPrefixLength = len;
Hans Boehm84614952014-11-25 18:46:17 -0800517 }
518 void write(DataOutput out) throws IOException {
519 out.writeBoolean(mDegreeMode);
520 }
521 }
522
523 private final CR RADIANS_PER_DEGREE = CR.PI.divide(CR.valueOf(180));
524
525 private final CR DEGREES_PER_RADIAN = CR.valueOf(180).divide(CR.PI);
526
527 private CR toRadians(CR x, EvalContext ec) {
528 if (ec.mDegreeMode) {
529 return x.multiply(RADIANS_PER_DEGREE);
530 } else {
531 return x;
532 }
533 }
534
535 private CR fromRadians(CR x, EvalContext ec) {
536 if (ec.mDegreeMode) {
537 return x.multiply(DEGREES_PER_RADIAN);
538 } else {
539 return x;
540 }
541 }
542
543 // The following methods can all throw IndexOutOfBoundsException
544 // in the event of a syntax error. We expect that to be caught in
545 // eval below.
546
Hans Boehmc023b732015-04-29 11:30:47 -0700547 private boolean isOperatorUnchecked(int i, int op) {
Hans Boehm84614952014-11-25 18:46:17 -0800548 Token t = mExpr.get(i);
549 if (!(t instanceof Operator)) return false;
550 return ((Operator)(t)).mId == op;
551 }
552
Hans Boehmc023b732015-04-29 11:30:47 -0700553 private boolean isOperator(int i, int op, EvalContext ec) {
554 if (i >= ec.mPrefixLength) return false;
555 return isOperatorUnchecked(i, op);
556 }
557
558 static class SyntaxException extends Exception {
559 public SyntaxException() {
Hans Boehm84614952014-11-25 18:46:17 -0800560 super();
561 }
Hans Boehmc023b732015-04-29 11:30:47 -0700562 public SyntaxException(String s) {
Hans Boehm84614952014-11-25 18:46:17 -0800563 super(s);
564 }
565 }
566
567 // The following functions all evaluate some kind of expression
568 // starting at position i in mExpr in a specified evaluation context.
569 // They return both the expression value (as constructive real and,
570 // if applicable, as BigInteger) and the position of the next token
571 // that was not used as part of the evaluation.
Hans Boehmc023b732015-04-29 11:30:47 -0700572 private EvalRet evalUnary(int i, EvalContext ec) throws SyntaxException {
Hans Boehm84614952014-11-25 18:46:17 -0800573 Token t = mExpr.get(i);
574 CR value;
575 if (t instanceof Constant) {
576 Constant c = (Constant)t;
Hans Boehm013969e2015-04-13 20:29:47 -0700577 value = CR.valueOf(c.toEasyString(),10);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700578 return new EvalRet(i+1, value, c.toRational());
Hans Boehm84614952014-11-25 18:46:17 -0800579 }
580 if (t instanceof PreEval) {
581 PreEval p = (PreEval)t;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700582 return new EvalRet(i+1, p.mValue, p.mRatValue);
Hans Boehm84614952014-11-25 18:46:17 -0800583 }
584 EvalRet argVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700585 BoundedRational ratVal;
Hans Boehm84614952014-11-25 18:46:17 -0800586 switch(((Operator)(t)).mId) {
587 case R.id.const_pi:
588 return new EvalRet(i+1, CR.PI, null);
589 case R.id.const_e:
Hans Boehm4db31b42015-05-31 12:19:05 -0700590 return new EvalRet(i+1, REAL_E, null);
Hans Boehm84614952014-11-25 18:46:17 -0800591 case R.id.op_sqrt:
Hans Boehmfbcef702015-04-27 18:07:47 -0700592 // Seems to have highest precedence.
593 // Does not add implicit paren.
594 // Does seem to accept a leading minus.
Hans Boehmc023b732015-04-29 11:30:47 -0700595 if (isOperator(i+1, R.id.op_sub, ec)) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700596 argVal = evalUnary(i+2, ec);
597 ratVal = BoundedRational.sqrt(
598 BoundedRational.negate(argVal.mRatVal));
599 if (ratVal != null) break;
600 return new EvalRet(argVal.mPos,
601 argVal.mVal.negate().sqrt(), null);
602 } else {
603 argVal = evalUnary(i+1, ec);
604 ratVal = BoundedRational.sqrt(argVal.mRatVal);
605 if (ratVal != null) break;
606 return new EvalRet(argVal.mPos, argVal.mVal.sqrt(), null);
607 }
Hans Boehm84614952014-11-25 18:46:17 -0800608 case R.id.lparen:
609 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700610 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700611 return new EvalRet(argVal.mPos, argVal.mVal, argVal.mRatVal);
Hans Boehm84614952014-11-25 18:46:17 -0800612 case R.id.fun_sin:
613 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700614 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm08e8f322015-04-21 13:18:38 -0700615 ratVal = ec.mDegreeMode ? BoundedRational.degreeSin(argVal.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700616 : BoundedRational.sin(argVal.mRatVal);
617 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800618 return new EvalRet(argVal.mPos,
Hans Boehm682ff5e2015-03-09 14:40:25 -0700619 toRadians(argVal.mVal,ec).sin(), null);
Hans Boehm84614952014-11-25 18:46:17 -0800620 case R.id.fun_cos:
621 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700622 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm08e8f322015-04-21 13:18:38 -0700623 ratVal = ec.mDegreeMode ? BoundedRational.degreeCos(argVal.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700624 : BoundedRational.cos(argVal.mRatVal);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700625 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800626 return new EvalRet(argVal.mPos,
Hans Boehm682ff5e2015-03-09 14:40:25 -0700627 toRadians(argVal.mVal,ec).cos(), null);
Hans Boehm84614952014-11-25 18:46:17 -0800628 case R.id.fun_tan:
629 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700630 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm08e8f322015-04-21 13:18:38 -0700631 ratVal = ec.mDegreeMode ? BoundedRational.degreeTan(argVal.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700632 : BoundedRational.tan(argVal.mRatVal);
633 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800634 CR argCR = toRadians(argVal.mVal, ec);
635 return new EvalRet(argVal.mPos,
Hans Boehm682ff5e2015-03-09 14:40:25 -0700636 argCR.sin().divide(argCR.cos()), null);
Hans Boehm84614952014-11-25 18:46:17 -0800637 case R.id.fun_ln:
638 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700639 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700640 ratVal = BoundedRational.ln(argVal.mRatVal);
641 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800642 return new EvalRet(argVal.mPos, argVal.mVal.ln(), null);
Hans Boehm4db31b42015-05-31 12:19:05 -0700643 case R.id.fun_exp:
644 argVal = evalExpr(i+1, ec);
645 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
646 ratVal = BoundedRational.exp(argVal.mRatVal);
647 if (ratVal != null) break;
648 return new EvalRet(argVal.mPos, argVal.mVal.exp(), null);
Hans Boehm84614952014-11-25 18:46:17 -0800649 case R.id.fun_log:
650 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700651 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700652 ratVal = BoundedRational.log(argVal.mRatVal);
653 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800654 return new EvalRet(argVal.mPos,
655 argVal.mVal.ln().divide(CR.valueOf(10).ln()),
656 null);
657 case R.id.fun_arcsin:
658 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700659 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm08e8f322015-04-21 13:18:38 -0700660 ratVal = ec.mDegreeMode ? BoundedRational.degreeAsin(argVal.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700661 : BoundedRational.asin(argVal.mRatVal);
662 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800663 return new EvalRet(argVal.mPos,
664 fromRadians(UnaryCRFunction
665 .asinFunction.execute(argVal.mVal),ec),
666 null);
667 case R.id.fun_arccos:
668 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700669 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm08e8f322015-04-21 13:18:38 -0700670 ratVal = ec.mDegreeMode ? BoundedRational.degreeAcos(argVal.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700671 : BoundedRational.acos(argVal.mRatVal);
672 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800673 return new EvalRet(argVal.mPos,
674 fromRadians(UnaryCRFunction
675 .acosFunction.execute(argVal.mVal),ec),
676 null);
677 case R.id.fun_arctan:
678 argVal = evalExpr(i+1, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700679 if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
Hans Boehm08e8f322015-04-21 13:18:38 -0700680 ratVal = ec.mDegreeMode ? BoundedRational.degreeAtan(argVal.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700681 : BoundedRational.atan(argVal.mRatVal);
682 if (ratVal != null) break;
Hans Boehm84614952014-11-25 18:46:17 -0800683 return new EvalRet(argVal.mPos,
684 fromRadians(UnaryCRFunction
685 .atanFunction.execute(argVal.mVal),ec),
686 null);
687 default:
Hans Boehmc023b732015-04-29 11:30:47 -0700688 throw new SyntaxException("Unrecognized token in expression");
Hans Boehm84614952014-11-25 18:46:17 -0800689 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700690 // We have a rational value.
691 return new EvalRet(argVal.mPos, ratVal.CRValue(), ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800692 }
693
694 // Compute an integral power of a constructive real.
695 // Unlike the "general" case using logarithms, this handles a negative
696 // base.
697 private static CR pow(CR base, BigInteger exp) {
698 if (exp.compareTo(BigInteger.ZERO) < 0) {
699 return pow(base, exp.negate()).inverse();
700 }
701 if (exp.equals(BigInteger.ONE)) return base;
702 if (exp.and(BigInteger.ONE).intValue() == 1) {
703 return pow(base, exp.subtract(BigInteger.ONE)).multiply(base);
704 }
705 if (exp.equals(BigInteger.ZERO)) {
706 return CR.valueOf(1);
707 }
708 CR tmp = pow(base, exp.shiftRight(1));
709 return tmp.multiply(tmp);
710 }
711
Hans Boehm682ff5e2015-03-09 14:40:25 -0700712 private static final int TEST_PREC = -100;
713 // Test for integer-ness to 100 bits past binary point.
714 private static final BigInteger MASK =
715 BigInteger.ONE.shiftLeft(-TEST_PREC).subtract(BigInteger.ONE);
Hans Boehm4db31b42015-05-31 12:19:05 -0700716 private static final CR REAL_E = CR.valueOf(1).exp();
717 private static final CR REAL_ONE_HUNDREDTH = CR.valueOf(100).inverse();
718 private static final BoundedRational RATIONAL_ONE_HUNDREDTH =
719 new BoundedRational(1,100);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700720 private static boolean isApprInt(CR x) {
721 BigInteger appr = x.get_appr(TEST_PREC);
722 return appr.and(MASK).signum() == 0;
723 }
724
Hans Boehm4db31b42015-05-31 12:19:05 -0700725 private EvalRet evalSuffix(int i, EvalContext ec) throws SyntaxException {
Hans Boehm84614952014-11-25 18:46:17 -0800726 EvalRet tmp = evalUnary(i, ec);
727 int cpos = tmp.mPos;
728 CR cval = tmp.mVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700729 BoundedRational ratVal = tmp.mRatVal;
Hans Boehm4db31b42015-05-31 12:19:05 -0700730 boolean isFact;
731 boolean isSquared = false;
732 while ((isFact = isOperator(cpos, R.id.op_fact, ec)) ||
733 (isSquared = isOperator(cpos, R.id.op_sqr, ec)) ||
734 isOperator(cpos, R.id.op_pct, ec)) {
735 if (isFact) {
736 if (ratVal == null) {
737 // Assume it was an integer, but we
738 // didn't figure it out.
739 // KitKat may have used the Gamma function.
740 if (!isApprInt(cval)) {
741 throw new ArithmeticException("factorial(non-integer)");
742 }
743 ratVal = new BoundedRational(cval.BigIntegerValue());
Hans Boehm682ff5e2015-03-09 14:40:25 -0700744 }
Hans Boehm4db31b42015-05-31 12:19:05 -0700745 ratVal = BoundedRational.fact(ratVal);
746 cval = ratVal.CRValue();
747 } else if (isSquared) {
748 ratVal = BoundedRational.multiply(ratVal, ratVal);
749 if (ratVal == null) {
750 cval = cval.multiply(cval);
751 } else {
752 cval = ratVal.CRValue();
753 }
754 } else /* percent */ {
755 ratVal = BoundedRational.multiply(ratVal, RATIONAL_ONE_HUNDREDTH);
756 if (ratVal == null) {
757 cval = cval.multiply(REAL_ONE_HUNDREDTH);
758 } else {
759 cval = ratVal.CRValue();
760 }
Hans Boehm84614952014-11-25 18:46:17 -0800761 }
Hans Boehm84614952014-11-25 18:46:17 -0800762 ++cpos;
763 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700764 return new EvalRet(cpos, cval, ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800765 }
766
Hans Boehmc023b732015-04-29 11:30:47 -0700767 private EvalRet evalFactor(int i, EvalContext ec) throws SyntaxException {
Hans Boehm4db31b42015-05-31 12:19:05 -0700768 final EvalRet result1 = evalSuffix(i, ec);
Hans Boehm84614952014-11-25 18:46:17 -0800769 int cpos = result1.mPos; // current position
770 CR cval = result1.mVal; // value so far
Hans Boehm682ff5e2015-03-09 14:40:25 -0700771 BoundedRational ratVal = result1.mRatVal; // int value so far
Hans Boehmc023b732015-04-29 11:30:47 -0700772 if (isOperator(cpos, R.id.op_pow, ec)) {
Hans Boehm84614952014-11-25 18:46:17 -0800773 final EvalRet exp = evalSignedFactor(cpos+1, ec);
774 cpos = exp.mPos;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700775 // Try completely rational evaluation first.
776 ratVal = BoundedRational.pow(ratVal, exp.mRatVal);
777 if (ratVal != null) {
778 return new EvalRet(cpos, ratVal.CRValue(), ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800779 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700780 // Power with integer exponent is defined for negative base.
781 // Thus we handle that case separately.
782 // We punt if the exponent is an integer computed from irrational
783 // values. That wouldn't work reliably with floating point either.
784 BigInteger int_exp = BoundedRational.asBigInteger(exp.mRatVal);
785 if (int_exp != null) {
786 cval = pow(cval, int_exp);
Hans Boehm84614952014-11-25 18:46:17 -0800787 } else {
Hans Boehm84614952014-11-25 18:46:17 -0800788 cval = cval.ln().multiply(exp.mVal).exp();
789 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700790 ratVal = null;
Hans Boehm84614952014-11-25 18:46:17 -0800791 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700792 return new EvalRet(cpos, cval, ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800793 }
794
Hans Boehmc023b732015-04-29 11:30:47 -0700795 private EvalRet evalSignedFactor(int i, EvalContext ec) throws SyntaxException {
796 final boolean negative = isOperator(i, R.id.op_sub, ec);
Hans Boehm08e8f322015-04-21 13:18:38 -0700797 int cpos = negative ? i + 1 : i;
Hans Boehm84614952014-11-25 18:46:17 -0800798 EvalRet tmp = evalFactor(cpos, ec);
799 cpos = tmp.mPos;
Hans Boehm08e8f322015-04-21 13:18:38 -0700800 CR cval = negative ? tmp.mVal.negate() : tmp.mVal;
801 BoundedRational ratVal = negative ? BoundedRational.negate(tmp.mRatVal)
Hans Boehm682ff5e2015-03-09 14:40:25 -0700802 : tmp.mRatVal;
803 return new EvalRet(cpos, cval, ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800804 }
805
806 private boolean canStartFactor(int i) {
807 if (i >= mExpr.size()) return false;
808 Token t = mExpr.get(i);
809 if (!(t instanceof Operator)) return true;
810 int id = ((Operator)(t)).mId;
811 if (KeyMaps.isBinary(id)) return false;
812 switch (id) {
813 case R.id.op_fact:
814 case R.id.rparen:
815 return false;
816 default:
817 return true;
818 }
819 }
820
Hans Boehmc023b732015-04-29 11:30:47 -0700821 private EvalRet evalTerm(int i, EvalContext ec) throws SyntaxException {
Hans Boehm84614952014-11-25 18:46:17 -0800822 EvalRet tmp = evalSignedFactor(i, ec);
823 boolean is_mul = false;
824 boolean is_div = false;
825 int cpos = tmp.mPos; // Current position in expression.
826 CR cval = tmp.mVal; // Current value.
Hans Boehm682ff5e2015-03-09 14:40:25 -0700827 BoundedRational ratVal = tmp.mRatVal; // Current rational value.
Hans Boehmc023b732015-04-29 11:30:47 -0700828 while ((is_mul = isOperator(cpos, R.id.op_mul, ec))
829 || (is_div = isOperator(cpos, R.id.op_div, ec))
Hans Boehm84614952014-11-25 18:46:17 -0800830 || canStartFactor(cpos)) {
831 if (is_mul || is_div) ++cpos;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700832 tmp = evalSignedFactor(cpos, ec);
Hans Boehm84614952014-11-25 18:46:17 -0800833 if (is_div) {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700834 ratVal = BoundedRational.divide(ratVal, tmp.mRatVal);
835 if (ratVal == null) {
Hans Boehm84614952014-11-25 18:46:17 -0800836 cval = cval.divide(tmp.mVal);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700837 } else {
838 cval = ratVal.CRValue();
Hans Boehm84614952014-11-25 18:46:17 -0800839 }
840 } else {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700841 ratVal = BoundedRational.multiply(ratVal, tmp.mRatVal);
842 if (ratVal == null) {
Hans Boehm84614952014-11-25 18:46:17 -0800843 cval = cval.multiply(tmp.mVal);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700844 } else {
845 cval = ratVal.CRValue();
Hans Boehm84614952014-11-25 18:46:17 -0800846 }
847 }
848 cpos = tmp.mPos;
849 is_mul = is_div = false;
850 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700851 return new EvalRet(cpos, cval, ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800852 }
853
Hans Boehmc023b732015-04-29 11:30:47 -0700854 private EvalRet evalExpr(int i, EvalContext ec) throws SyntaxException {
Hans Boehm84614952014-11-25 18:46:17 -0800855 EvalRet tmp = evalTerm(i, ec);
856 boolean is_plus;
857 int cpos = tmp.mPos;
858 CR cval = tmp.mVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700859 BoundedRational ratVal = tmp.mRatVal;
Hans Boehmc023b732015-04-29 11:30:47 -0700860 while ((is_plus = isOperator(cpos, R.id.op_add, ec))
861 || isOperator(cpos, R.id.op_sub, ec)) {
Hans Boehm84614952014-11-25 18:46:17 -0800862 tmp = evalTerm(cpos+1, ec);
863 if (is_plus) {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700864 ratVal = BoundedRational.add(ratVal, tmp.mRatVal);
865 if (ratVal == null) {
Hans Boehm84614952014-11-25 18:46:17 -0800866 cval = cval.add(tmp.mVal);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700867 } else {
868 cval = ratVal.CRValue();
Hans Boehm84614952014-11-25 18:46:17 -0800869 }
870 } else {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700871 ratVal = BoundedRational.subtract(ratVal, tmp.mRatVal);
872 if (ratVal == null) {
Hans Boehm84614952014-11-25 18:46:17 -0800873 cval = cval.subtract(tmp.mVal);
Hans Boehm682ff5e2015-03-09 14:40:25 -0700874 } else {
875 cval = ratVal.CRValue();
Hans Boehm84614952014-11-25 18:46:17 -0800876 }
877 }
878 cpos = tmp.mPos;
879 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700880 return new EvalRet(cpos, cval, ratVal);
Hans Boehm84614952014-11-25 18:46:17 -0800881 }
882
883 // Externally visible evaluation result.
884 public class EvalResult {
Hans Boehm682ff5e2015-03-09 14:40:25 -0700885 EvalResult (CR val, BoundedRational ratVal) {
Hans Boehm84614952014-11-25 18:46:17 -0800886 mVal = val;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700887 mRatVal = ratVal;
Hans Boehm84614952014-11-25 18:46:17 -0800888 }
889 final CR mVal;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700890 final BoundedRational mRatVal;
Hans Boehm84614952014-11-25 18:46:17 -0800891 }
892
Hans Boehmc023b732015-04-29 11:30:47 -0700893 // Return the starting position of the sequence of trailing operators
894 // that cannot be meaningfully evaluated.
895 private int trailingOpsStart() {
896 int result = mExpr.size();
897 while (result > 0) {
898 Token last = mExpr.get(result - 1);
899 if (!(last instanceof Operator)) break;
900 Operator o = (Operator)last;
901 if (KeyMaps.isSuffix(o.mId) || o.mId == R.id.const_pi
902 || o.mId == R.id.const_e) {
903 break;
904 }
905 --result;
906 }
907 return result;
908 }
909
910 public boolean hasTrailingOperators() {
911 return trailingOpsStart() != mExpr.size();
912 }
913
914 // Is the current expression worth evaluating?
915 public boolean hasInterestingOps() {
916 int last = trailingOpsStart();
917 int first = 0;
918 if (last > first && isOperatorUnchecked(first, R.id.op_sub)) {
919 // Leading minus is not by itself interesting.
920 first++;
921 }
922 for (int i = first; i < last; ++i) {
923 Token t1 = mExpr.get(i);
Hans Boehm187d3e92015-06-09 18:04:26 -0700924 if (t1 instanceof Operator
925 || t1 instanceof PreEval && ((PreEval)t1).hasEllipsis()) {
926 return true;
927 }
Hans Boehmc023b732015-04-29 11:30:47 -0700928 }
929 return false;
930 }
931
Hans Boehm84614952014-11-25 18:46:17 -0800932 // Evaluate the entire expression, returning null in the event
933 // of an error.
934 // Not called from the UI thread, but should not be called
935 // concurrently with modifications to the expression.
Hans Boehmc023b732015-04-29 11:30:47 -0700936 EvalResult eval(boolean degreeMode, boolean required) throws SyntaxException
937 // And unchecked exceptions thrown by CR
938 // and BoundedRational.
Hans Boehm84614952014-11-25 18:46:17 -0800939 {
940 try {
Hans Boehmc023b732015-04-29 11:30:47 -0700941 int prefixLen = required ? mExpr.size() : trailingOpsStart();
942 EvalContext ec = new EvalContext(degreeMode, prefixLen);
Hans Boehm84614952014-11-25 18:46:17 -0800943 EvalRet res = evalExpr(0, ec);
Hans Boehmc023b732015-04-29 11:30:47 -0700944 if (res.mPos != prefixLen) {
945 throw new SyntaxException("Failed to parse full expression");
Hans Boehmfbcef702015-04-27 18:07:47 -0700946 }
Hans Boehm682ff5e2015-03-09 14:40:25 -0700947 return new EvalResult(res.mVal, res.mRatVal);
Hans Boehm84614952014-11-25 18:46:17 -0800948 } catch (IndexOutOfBoundsException e) {
Hans Boehmc023b732015-04-29 11:30:47 -0700949 throw new SyntaxException("Unexpected expression end");
Hans Boehm84614952014-11-25 18:46:17 -0800950 }
951 }
952
953 // Produce a string representation of the expression itself
954 String toString(Context context) {
955 StringBuilder sb = new StringBuilder();
956 for (Token t: mExpr) {
957 sb.append(t.toString(context));
958 }
959 return sb.toString();
960 }
961}