• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// A token that composes an expression. There are several kinds of tokens
6/// that represent arithmetic operation symbols, numbers and pieces of numbers.
7/// We need to represent pieces of numbers because the user may have only
8/// entered a partial expression so far.
9class ExpressionToken {
10  ExpressionToken(this.stringRep);
11
12  final String stringRep;
13
14  @override
15  String toString() => stringRep;
16}
17
18/// A token that represents a number.
19class NumberToken extends ExpressionToken {
20  NumberToken(String stringRep, this.number) : super(stringRep);
21
22  NumberToken.fromNumber(num number) : this('$number', number);
23
24  final num number;
25}
26
27/// A token that represents an integer.
28class IntToken extends NumberToken {
29  IntToken(String stringRep) : super(stringRep, int.parse(stringRep));
30}
31
32/// A token that represents a floating point number.
33class FloatToken extends NumberToken {
34  FloatToken(String stringRep) : super(stringRep, _parse(stringRep));
35
36  static double _parse(String stringRep) {
37    String toParse = stringRep;
38    if (toParse.startsWith('.'))
39      toParse = '0' + toParse;
40    if (toParse.endsWith('.'))
41      toParse = toParse + '0';
42    return double.parse(toParse);
43  }
44}
45
46/// A token that represents a number that is the result of a computation.
47class ResultToken extends NumberToken {
48  ResultToken(num number) : super.fromNumber(round(number));
49
50  /// rounds `number` to 14 digits of precision. A double precision
51  /// floating point number is guaranteed to have at least this many
52  /// decimal digits of precision.
53  static num round(num number) {
54    if (number is int)
55      return number;
56    return double.parse(number.toStringAsPrecision(14));
57  }
58}
59
60/// A token that represents the unary minus prefix.
61class LeadingNegToken extends ExpressionToken {
62  LeadingNegToken() : super('-');
63}
64
65enum Operation { Addition, Subtraction, Multiplication, Division }
66
67/// A token that represents an arithmetic operation symbol.
68class OperationToken extends ExpressionToken {
69  OperationToken(this.operation)
70   : super(opString(operation));
71
72  Operation operation;
73
74  static String opString(Operation operation) {
75    switch (operation) {
76      case Operation.Addition:
77        return ' + ';
78      case Operation.Subtraction:
79        return ' - ';
80      case Operation.Multiplication:
81        return '  \u00D7  ';
82      case Operation.Division:
83        return '  \u00F7  ';
84    }
85    assert(operation != null);
86    return null;
87  }
88}
89
90/// As the user taps different keys the current expression can be in one
91/// of several states.
92enum ExpressionState {
93  /// The expression is empty or an operation symbol was just entered.
94  /// A new number must be started now.
95  Start,
96
97  /// A minus sign was entered as a leading negative prefix.
98  LeadingNeg,
99
100  /// We are in the midst of a number without a point.
101  Number,
102
103  /// A point was just entered.
104  Point,
105
106  /// We are in the midst of a number with a point.
107  NumberWithPoint,
108
109  /// A result is being displayed
110  Result,
111}
112
113/// An expression that can be displayed in a calculator. It is the result
114/// of a sequence of user entries. It is represented by a sequence of tokens.
115///
116/// The tokens are not in one to one correspondence with the key taps because we
117/// use one token per number, not one token per digit. A [CalcExpression] is
118/// immutable. The `append*` methods return a new [CalcExpression] that
119/// represents the appropriate expression when one additional key tap occurs.
120class CalcExpression {
121  CalcExpression(this._list, this.state);
122
123  CalcExpression.empty()
124    : this(<ExpressionToken>[], ExpressionState.Start);
125
126  CalcExpression.result(FloatToken result)
127    : _list = <ExpressionToken>[],
128      state = ExpressionState.Result {
129    _list.add(result);
130  }
131
132  /// The tokens comprising the expression.
133  final List<ExpressionToken> _list;
134  /// The state of the expression.
135  final ExpressionState state;
136
137  /// The string representation of the expression. This will be displayed
138  /// in the calculator's display panel.
139  @override
140  String toString() {
141    final StringBuffer buffer = StringBuffer('');
142    buffer.writeAll(_list);
143    return buffer.toString();
144  }
145
146  /// Append a digit to the current expression and return a new expression
147  /// representing the result. Returns null to indicate that it is not legal
148  /// to append a digit in the current state.
149  CalcExpression appendDigit(int digit) {
150    ExpressionState newState = ExpressionState.Number;
151    ExpressionToken newToken;
152    final List<ExpressionToken> outList = _list.toList();
153    switch (state) {
154      case ExpressionState.Start:
155        // Start a new number with digit.
156        newToken = IntToken('$digit');
157        break;
158      case ExpressionState.LeadingNeg:
159        // Replace the leading neg with a negative number starting with digit.
160        outList.removeLast();
161        newToken = IntToken('-$digit');
162        break;
163      case ExpressionState.Number:
164        final ExpressionToken last = outList.removeLast();
165        newToken = IntToken('${last.stringRep}$digit');
166        break;
167      case ExpressionState.Point:
168      case ExpressionState.NumberWithPoint:
169        final ExpressionToken last = outList.removeLast();
170        newState = ExpressionState.NumberWithPoint;
171        newToken = FloatToken('${last.stringRep}$digit');
172        break;
173      case ExpressionState.Result:
174        // Cannot enter a number now
175        return null;
176    }
177    outList.add(newToken);
178    return CalcExpression(outList, newState);
179  }
180
181  /// Append a point to the current expression and return a new expression
182  /// representing the result. Returns null to indicate that it is not legal
183  /// to append a point in the current state.
184  CalcExpression appendPoint() {
185    ExpressionToken newToken;
186    final List<ExpressionToken> outList = _list.toList();
187    switch (state) {
188      case ExpressionState.Start:
189        newToken = FloatToken('.');
190        break;
191      case ExpressionState.LeadingNeg:
192      case ExpressionState.Number:
193        final ExpressionToken last = outList.removeLast();
194        newToken = FloatToken(last.stringRep + '.');
195        break;
196      case ExpressionState.Point:
197      case ExpressionState.NumberWithPoint:
198      case ExpressionState.Result:
199        // Cannot enter a point now
200        return null;
201    }
202    outList.add(newToken);
203    return CalcExpression(outList, ExpressionState.Point);
204  }
205
206  /// Append an operation symbol to the current expression and return a new
207  /// expression representing the result. Returns null to indicate that it is not
208  /// legal to append an operation symbol in the current state.
209  CalcExpression appendOperation(Operation op) {
210    switch (state) {
211      case ExpressionState.Start:
212      case ExpressionState.LeadingNeg:
213      case ExpressionState.Point:
214        // Cannot enter operation now.
215        return null;
216      case ExpressionState.Number:
217      case ExpressionState.NumberWithPoint:
218      case ExpressionState.Result:
219        break;
220    }
221    final List<ExpressionToken> outList = _list.toList();
222    outList.add(OperationToken(op));
223    return CalcExpression(outList, ExpressionState.Start);
224  }
225
226  /// Append a leading minus sign to the current expression and return a new
227  /// expression representing the result. Returns null to indicate that it is not
228  /// legal to append a leading minus sign in the current state.
229  CalcExpression appendLeadingNeg() {
230    switch (state) {
231      case ExpressionState.Start:
232        break;
233      case ExpressionState.LeadingNeg:
234      case ExpressionState.Point:
235      case ExpressionState.Number:
236      case ExpressionState.NumberWithPoint:
237      case ExpressionState.Result:
238        // Cannot enter leading neg now.
239        return null;
240    }
241    final List<ExpressionToken> outList = _list.toList();
242    outList.add(LeadingNegToken());
243    return CalcExpression(outList, ExpressionState.LeadingNeg);
244  }
245
246  /// Append a minus sign to the current expression and return a new expression
247  /// representing the result. Returns null to indicate that it is not legal
248  /// to append a minus sign in the current state. Depending on the current
249  /// state the minus sign will be interpreted as either a leading negative
250  /// sign or a subtraction operation.
251  CalcExpression appendMinus() {
252    switch (state) {
253      case ExpressionState.Start:
254        return appendLeadingNeg();
255      case ExpressionState.LeadingNeg:
256      case ExpressionState.Point:
257      case ExpressionState.Number:
258      case ExpressionState.NumberWithPoint:
259      case ExpressionState.Result:
260        return appendOperation(Operation.Subtraction);
261      default:
262        return null;
263    }
264  }
265
266  /// Computes the result of the current expression and returns a new
267  /// ResultExpression containing the result. Returns null to indicate that
268  /// it is not legal to compute a result in the current state.
269  CalcExpression computeResult() {
270    switch (state) {
271      case ExpressionState.Start:
272      case ExpressionState.LeadingNeg:
273      case ExpressionState.Point:
274      case ExpressionState.Result:
275        // Cannot compute result now.
276        return null;
277      case ExpressionState.Number:
278      case ExpressionState.NumberWithPoint:
279        break;
280    }
281
282    // We make a copy of _list because CalcExpressions are supposed to
283    // be immutable.
284    final List<ExpressionToken> list = _list.toList();
285    // We obey order-of-operations by computing the sum of the 'terms',
286    // where a "term" is defined to be a sequence of numbers separated by
287    // multiplication or division symbols.
288    num currentTermValue = removeNextTerm(list);
289    while (list.isNotEmpty) {
290      final OperationToken opToken = list.removeAt(0);
291      final num nextTermValue = removeNextTerm(list);
292      switch (opToken.operation) {
293        case Operation.Addition:
294          currentTermValue += nextTermValue;
295          break;
296        case Operation.Subtraction:
297          currentTermValue -= nextTermValue;
298          break;
299        case Operation.Multiplication:
300        case Operation.Division:
301          // Logic error.
302          assert(false);
303      }
304    }
305    final List<ExpressionToken> outList = <ExpressionToken>[];
306    outList.add(ResultToken(currentTermValue));
307    return CalcExpression(outList, ExpressionState.Result);
308  }
309
310  /// Removes the next "term" from `list` and returns its numeric value.
311  /// A "term" is a sequence of number tokens separated by multiplication
312  /// and division symbols.
313  static num removeNextTerm(List<ExpressionToken> list) {
314    assert(list != null && list.isNotEmpty);
315    final NumberToken firstNumToken = list.removeAt(0);
316    num currentValue = firstNumToken.number;
317    while (list.isNotEmpty) {
318      bool isDivision = false;
319      final OperationToken nextOpToken = list.first;
320      switch (nextOpToken.operation) {
321        case Operation.Addition:
322        case Operation.Subtraction:
323          // We have reached the end of the current term
324          return currentValue;
325        case Operation.Multiplication:
326          break;
327        case Operation.Division:
328          isDivision = true;
329      }
330      // Remove the operation token.
331      list.removeAt(0);
332      // Remove the next number token.
333      final NumberToken nextNumToken = list.removeAt(0);
334      final num nextNumber = nextNumToken.number;
335      if (isDivision)
336        currentValue /= nextNumber;
337      else
338        currentValue *= nextNumber;
339    }
340    return currentValue;
341  }
342}
343