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