logic.dart 11.4 KB
Newer Older
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
// Copyright 2016 The Chromium 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;
38 39 40 41
    if (toParse.startsWith('.'))
      toParse = '0' + toParse;
    if (toParse.endsWith('.'))
      toParse = toParse + '0';
42 43 44 45 46 47 48 49
    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));

50 51 52
  /// 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.
53
  static num round(num number) {
54 55
    if (number is int)
      return number;
56 57 58 59 60 61 62 63 64 65 66 67 68
    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 {
69 70
  OperationToken(this.operation)
   : super(opString(operation));
71 72 73 74 75 76 77 78 79 80 81 82 83 84

  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  ';
    }
pq's avatar
pq committed
85
    assert(operation != null);
pq's avatar
pq committed
86
    return null;
87 88 89 90 91 92
  }
}

/// As the user taps different keys the current expression can be in one
/// of several states.
enum ExpressionState {
93 94
  /// The expression is empty or an operation symbol was just entered.
  /// 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 111 112 113 114 115 116 117 118 119 120 121 122
  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.
/// Note that 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);

123
  CalcExpression.empty()
124
    : this(<ExpressionToken>[], ExpressionState.Start);
125

126
  CalcExpression.result(FloatToken result)
127 128
    : _list = <ExpressionToken>[],
      state = ExpressionState.Result {
129 130 131
    _list.add(result);
  }

132
  /// The tokens comprising the expression.
133
  final List<ExpressionToken> _list;
134
  /// The state of the expression.
135 136
  final ExpressionState state;

137 138
  /// The string representation of the expression. This will be displayed
  /// in the calculator's display panel.
139 140
  @override
  String toString() {
141
    final StringBuffer buffer = new StringBuffer('');
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    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 = new IntToken('$digit');
        break;
      case ExpressionState.LeadingNeg:
        // Replace the leading neg with a negative number starting with digit.
        outList.removeLast();
        newToken = new IntToken('-$digit');
        break;
      case ExpressionState.Number:
164
        final ExpressionToken last = outList.removeLast();
165 166 167 168
        newToken = new IntToken('${last.stringRep}$digit');
        break;
      case ExpressionState.Point:
      case ExpressionState.NumberWithPoint:
169
        final ExpressionToken last = outList.removeLast();
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
        newState = ExpressionState.NumberWithPoint;
        newToken = new FloatToken('${last.stringRep}$digit');
        break;
      case ExpressionState.Result:
        // Cannot enter a number now
        return null;
    }
    outList.add(newToken);
    return new 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 = new FloatToken('.');
        break;
      case ExpressionState.LeadingNeg:
      case ExpressionState.Number:
193
        final ExpressionToken last = outList.removeLast();
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
        newToken = new FloatToken(last.stringRep + '.');
        break;
      case ExpressionState.Point:
      case ExpressionState.NumberWithPoint:
      case ExpressionState.Result:
        // Cannot enter a point now
        return null;
    }
    outList.add(newToken);
    return new 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(new OperationToken(op));
    return new 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(new LeadingNegToken());
    return new 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
249
  /// state the minus sign will be interpreted as either a leading negative
250 251 252 253 254 255 256 257 258 259 260
  /// 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);
pq's avatar
pq committed
261 262
      default:
        return null;
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
    }
  }

  /// 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.
284
    final List<ExpressionToken> list = _list.toList();
285 286
    // 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
Josh Soref's avatar
Josh Soref committed
287
    // multiplication or division symbols.
288
    num currentTermValue = removeNextTerm(list);
289
    while (list.isNotEmpty) {
290 291
      final OperationToken opToken = list.removeAt(0);
      final num nextTermValue = removeNextTerm(list);
292 293 294 295 296 297 298 299 300 301 302 303 304
      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);
      }
    }
305
    final List<ExpressionToken> outList = <ExpressionToken>[];
306 307 308 309
    outList.add(new ResultToken(currentTermValue));
    return new CalcExpression(outList, ExpressionState.Result);
  }

310 311 312
  /// 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.
313
  static num removeNextTerm(List<ExpressionToken> list) {
314
    assert(list != null && list.isNotEmpty);
315 316
    final NumberToken firstNumToken = list.removeAt(0);
    num currentValue = firstNumToken.number;
317
    while (list.isNotEmpty) {
318
      bool isDivision = false;
319
      final OperationToken nextOpToken = list.first;
320 321 322 323 324 325 326 327 328 329 330 331 332 333
      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);
334
      final num nextNumber = nextNumToken.number;
335 336 337 338 339 340 341 342
      if (isDivision)
        currentValue /= nextNumber;
      else
        currentValue *= nextNumber;
    }
    return currentValue;
  }
}