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