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