// 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 stringRep, this.number) : super(stringRep); 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 '; } assert(operation != null); return null; } } /// 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(); newToken = FloatToken(last.stringRep + '.'); 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); default: return null; } } /// 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 != null && 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; } }