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 69
    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(Operation operation)
70 71
   : operation = operation,
     super(opString(operation));
72 73 74 75 76 77 78 79 80 81 82 83 84 85

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

/// As the user taps different keys the current expression can be in one
/// of several states.
enum ExpressionState {
94 95
  /// The expression is empty or an operation symbol was just entered.
  /// A new number must be started now.
96
  Start,
97 98

  /// A minus sign was entered as a leading negative prefix.
99
  LeadingNeg,
100 101

  /// We are in the midst of a number without a point.
102
  Number,
103 104

  /// A point was just entered.
105
  Point,
106 107

  /// We are in the midst of a number with a point.
108
  NumberWithPoint,
109 110

  /// A result is being displayed
111 112 113 114 115 116 117 118 119 120 121 122 123 124
  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);

  CalcExpression.Empty()
125
    : this(<ExpressionToken>[], ExpressionState.Start);
126 127

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

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

138 139
  /// The string representation of the expression. This will be displayed
  /// in the calculator's display panel.
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
  @override
  String toString() {
    StringBuffer buffer = new 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 = 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:
        ExpressionToken last = outList.removeLast();
        newToken = new IntToken('${last.stringRep}$digit');
        break;
      case ExpressionState.Point:
      case ExpressionState.NumberWithPoint:
        ExpressionToken last = outList.removeLast();
        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:
        ExpressionToken last = outList.removeLast();
        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
  /// state the minus sign will be interpretted 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);
pq's avatar
pq committed
262 263
      default:
        return null;
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
    }
  }

  /// 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.
    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
    // multiplcation or division symbols.
    num currentTermValue = removeNextTerm(list);
    while (list.length > 0) {
      OperationToken opToken = list.removeAt(0);
      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 = new List<ExpressionToken>();
    outList.add(new ResultToken(currentTermValue));
    return new CalcExpression(outList, ExpressionState.Result);
  }

311 312 313
  /// 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.
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 341 342 343
  static num removeNextTerm(List<ExpressionToken> list) {
    assert(list != null && list.length >= 1);
    final NumberToken firstNumToken = list.removeAt(0);
    num currentValue = firstNumToken.number;
    while (list.length > 0) {
      bool isDivision = false;
      OperationToken nextOpToken = list.first;
      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);
      num nextNumber = nextNumToken.number;
      if (isDivision)
        currentValue /= nextNumber;
      else
        currentValue *= nextNumber;
    }
    return currentValue;
  }
}