logic.dart 11.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11
// 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);

12
  final String? stringRep;
13 14

  @override
15
  String toString() => stringRep!;
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
}

/// 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
    if (toParse.startsWith('.'))
39
      toParse = '0$toParse';
40
    if (toParse.endsWith('.'))
41
      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

  Operation operation;

74
  static String? opString(Operation operation) {
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
    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 {
91 92
  /// The expression is empty or an operation symbol was just entered.
  /// A new number must be started now.
93
  Start,
94 95

  /// A minus sign was entered as a leading negative prefix.
96
  LeadingNeg,
97 98

  /// We are in the midst of a number without a point.
99
  Number,
100 101

  /// A point was just entered.
102
  Point,
103 104

  /// We are in the midst of a number with a point.
105
  NumberWithPoint,
106 107

  /// A result is being displayed
108 109 110 111 112
  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.
Ian Hickson's avatar
Ian Hickson committed
113 114 115 116 117
///
/// 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.
118 119 120
class CalcExpression {
  CalcExpression(this._list, this.state);

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

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

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

135 136
  /// The string representation of the expression. This will be displayed
  /// in the calculator's display panel.
137 138
  @override
  String toString() {
139
    final StringBuffer buffer = StringBuffer();
140 141 142 143 144 145 146
    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.
147
  CalcExpression? appendDigit(int digit) {
148
    ExpressionState newState = ExpressionState.Number;
149 150
    ExpressionToken? newToken;
    final List<ExpressionToken?> outList = _list.toList();
151 152 153
    switch (state) {
      case ExpressionState.Start:
        // Start a new number with digit.
154
        newToken = IntToken('$digit');
155 156 157 158
        break;
      case ExpressionState.LeadingNeg:
        // Replace the leading neg with a negative number starting with digit.
        outList.removeLast();
159
        newToken = IntToken('-$digit');
160 161
        break;
      case ExpressionState.Number:
162
        final ExpressionToken last = outList.removeLast()!;
163
        newToken = IntToken('${last.stringRep}$digit');
164 165 166
        break;
      case ExpressionState.Point:
      case ExpressionState.NumberWithPoint:
167
        final ExpressionToken last = outList.removeLast()!;
168
        newState = ExpressionState.NumberWithPoint;
169
        newToken = FloatToken('${last.stringRep}$digit');
170 171 172 173 174 175
        break;
      case ExpressionState.Result:
        // Cannot enter a number now
        return null;
    }
    outList.add(newToken);
176
    return CalcExpression(outList, newState);
177 178 179 180 181
  }

  /// 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.
182 183 184
  CalcExpression? appendPoint() {
    ExpressionToken? newToken;
    final List<ExpressionToken?> outList = _list.toList();
185 186
    switch (state) {
      case ExpressionState.Start:
187
        newToken = FloatToken('.');
188 189 190
        break;
      case ExpressionState.LeadingNeg:
      case ExpressionState.Number:
191
        final ExpressionToken last = outList.removeLast()!;
192 193
        final String value = last.stringRep!;
        newToken = FloatToken('$value.');
194 195 196 197 198 199 200 201
        break;
      case ExpressionState.Point:
      case ExpressionState.NumberWithPoint:
      case ExpressionState.Result:
        // Cannot enter a point now
        return null;
    }
    outList.add(newToken);
202
    return CalcExpression(outList, ExpressionState.Point);
203 204 205 206 207
  }

  /// 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.
208
  CalcExpression? appendOperation(Operation op) {
209 210 211 212 213 214 215 216 217 218 219
    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;
    }
220
    final List<ExpressionToken?> outList = _list.toList();
221 222
    outList.add(OperationToken(op));
    return CalcExpression(outList, ExpressionState.Start);
223 224 225 226 227
  }

  /// 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.
228
  CalcExpression? appendLeadingNeg() {
229 230 231 232 233 234 235 236 237 238 239
    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;
    }
240
    final List<ExpressionToken?> outList = _list.toList();
241 242
    outList.add(LeadingNegToken());
    return CalcExpression(outList, ExpressionState.LeadingNeg);
243 244 245 246 247
  }

  /// 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
248
  /// state the minus sign will be interpreted as either a leading negative
249
  /// sign or a subtraction operation.
250
  CalcExpression? appendMinus() {
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
    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.
266
  CalcExpression? computeResult() {
267 268 269 270 271 272 273 274 275 276 277 278 279 280
    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.
281
    final List<ExpressionToken?> list = _list.toList();
282 283
    // 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
284
    // multiplication or division symbols.
285
    num currentTermValue = removeNextTerm(list);
286
    while (list.isNotEmpty) {
287
      final OperationToken opToken = list.removeAt(0)! as OperationToken;
288
      final num nextTermValue = removeNextTerm(list);
289 290 291 292 293 294 295 296 297 298 299 300 301
      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);
      }
    }
302 303 304
    final List<ExpressionToken> outList = <ExpressionToken>[
      ResultToken(currentTermValue),
    ];
305
    return CalcExpression(outList, ExpressionState.Result);
306 307
  }

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