1/**
2 * Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file.
5 **/
6
7function Model(precision) {
8  this.reset_({precision: precision});
9}
10
11/**
12 * Handles a calculator key input, updating the calculator state accordingly and
13 * returning an object with 'accumulator', 'operator', and 'operand' properties
14 * representing that state.
15 *
16 * @private
17 */
18Model.prototype.handle = function(input) {
19  switch (input) {
20    case '+':
21    case '-':
22    case '/':
23    case '*':
24      // For operations, ignore the last operator if no operand was entered,
25      // otherwise perform the current calculation before setting the new
26      // operator. In either case, clear the operand and the defaults.
27      var operator = this.operand && this.operator;
28      var result = this.calculate_(operator, this.operand);
29      return this.reset_({accumulator: result, operator: input});
30    case '=':
31      // For the equal sign, perform the current calculation and save the
32      // operator and operands used as defaults, or if there is no current
33      // operator, use the default operators and operands instead. In any case,
34      // clear the operator and operand and return a transient state with a '='
35      // operator.
36      var operator = this.operator || this.defaults.operator;
37      var operand = this.operator ? this.operand : this.defaults.operand;
38      var result = this.calculate_(operator, operand);
39      var defaults = {operator: operator, operand: this.operand};
40      return this.reset_({accumulator: result, defaults: defaults});
41    case 'AC':
42      return this.reset_({});
43    case 'C':
44      return this.operand  ? this.set_({operand: null}) :
45             this.operator ? this.set_({operator: null}) :
46                             this.handle('AC');
47    case 'back':
48      var length = (this.operand || '').length;
49      return (length > 1)  ? this.set_({operand: this.operand.slice(0, -1)}) :
50             this.operand  ? this.set_({operand: null}) :
51                             this.set_({operator: null});
52    case '+ / -':
53      var initial = (this.operand || '0')[0];
54      return (initial === '-') ? this.set_({operand: this.operand.slice(1)}) :
55             (initial !== '0') ? this.set_({operand: '-' + this.operand}) :
56                                 this.set_({});
57    default:
58      var operand = (this.operand || '0') + input;
59      var duplicate = (operand.replace(/[^.]/g, '').length > 1);
60      var overflow = (operand.replace(/[^0-9]/g, '').length > this.precision);
61      return operand.match(/^0[0-9]/)  ? this.set_({operand: operand[1]}) :
62             (!duplicate && !overflow) ? this.set_({operand: operand}) :
63                                         this.set_({});
64  }
65}
66
67/**
68 * Reset the model's state to the passed in state.
69 *
70 * @private
71 */
72Model.prototype.reset_ = function(state) {
73  this.accumulator = this.operand = this.operator = null;
74  this.defaults = {operator: null, operand: null};
75  return this.set_(state);
76}
77
78/**
79 * Selectively replace the model's state with the passed in state.
80 *
81 * @private
82 */
83Model.prototype.set_ = function(state) {
84  var ifDefined = function(x, y) { return (x !== undefined) ? x : y; };
85  var precision = (state && state.precision) || this.precision || 9;
86  this.precision = Math.min(Math.max(precision, 1), 9);
87  this.accumulator = ifDefined(state && state.accumulator, this.accumulator);
88  this.operator = ifDefined(state && state.operator, this.operator);
89  this.operand = ifDefined(state && state.operand, this.operand);
90  this.defaults = ifDefined(state && state.defaults, this.defaults);
91  return this;
92}
93
94/**
95 * Performs a calculation based on the passed in operator and operand, updating
96 * the model's state with the operator and operand used but returning the result
97 * of the calculation instead of updating the model's state with it.
98 *
99 * @private
100 */
101Model.prototype.calculate_ = function(operator, operand) {
102  var x = Number(this.accumulator) || 0;
103  var y = operand ? Number(operand) : x;
104  this.set_({accumulator: String(x), operator: operator, operand: String(y)});
105  return (this.operator == '+') ? this.round_(x + y) :
106         (this.operator == '-') ? this.round_(x - y) :
107         (this.operator == '*') ? this.round_(x * y) :
108         (this.operator == '/') ? this.round_(x / y) :
109                                  this.round_(y);
110}
111
112/**
113 * Returns the string representation of the passed in value rounded to the
114 * model's precision, or "E" on overflow.
115 *
116 * @private
117 */
118Model.prototype.round_ = function(x) {
119  var exponent = Number(x.toExponential(this.precision - 1).split('e')[1]);
120  var digits = this.digits_(exponent);
121  var exponential = x.toExponential(digits).replace(/\.?0+e/, 'e');
122  var fixed = (Math.abs(exponent) < this.precision && exponent > -7);
123  return !digits ? 'E' : fixed ? String(Number(exponential)) : exponential;
124}
125
126/**
127 * Returns the appropriate number of digits to include of a number based on
128 * its size.
129 *
130 * @private
131 */
132Model.prototype.digits_ = function(exponent) {
133  return (isNaN(exponent) || exponent < -199 || exponent > 199) ? 0 :
134         (exponent < -99) ? (this.precision - 1 - 5) :
135         (exponent < -9) ? (this.precision - 1 - 4) :
136         (exponent < -6) ? (this.precision - 1 - 3) :
137         (exponent < 0) ? (this.precision - 1 + exponent) :
138         (exponent < this.precision) ? (this.precision - 1) :
139         (exponent < 10) ? (this.precision - 1 - 3) :
140         (exponent < 100) ? (this.precision - 1 - 4) :
141                            (this.precision - 1 - 5);
142}
143