-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcalculation.js
393 lines (357 loc) · 12.2 KB
/
calculation.js
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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
262
263
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
311
312
313
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
/*
* jQuery Calculation Plug-in
*
* Copyright (c) 2007 Dan G. Switzer, II
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Revision: 12
* Version: 0.4.08
*
* Revision History
* v0.4.08
* - Added missing semi-colon to lines
*
* v0.4.07
* - Added trim to parseNumber to fix issue with whitespace in elements
*
* v0.4.06
* - Added support for calc() "format" callback so that if return value
* is null, then value is not updated
* - Added jQuery.isFunction() check for calc() callbacks
*
* v0.4.05
* - Added support to the sum() & calc() method for automatically fixing precision
* issues (will detect the max decimal spot in the number and fix to that
* depth)
*
* v0.4.04
* - Fixed bug #5420 by adding the defaults.cleanseNumber handler; you can
* override this function to handle stripping number of extra digits
*
* v0.4.02
* - Fixed bug where bind parameter was not being detecting if you specified
* a string in method like sum(), avg(), etc.
*
* v0.4a
* - Fixed bug in aggregate functions so that a string is passed to jQuery's
* text() method (since numeric zero is interpetted as false)
*
* v0.4
* - Added support for -$.99 values
* - Fixed regex so that decimal values without leading zeros are correctly
* parsed
* - Removed defaults.comma setting
* - Changed secondary regex that cleans additional formatting from parsed
* number
*
* v0.3
* - Refactored the aggregate methods (since they all use the same core logic)
* to use the $.extend() method
* - Added support for negative numbers in the regex)
* - Added min/max aggregate methods
* - Added defaults.onParseError and defaults.onParseClear methods to add logic for
* parsing errors
*
* v0.2
* - Fixed bug in sMethod in calc() (was using getValue, should have been setValue)
* - Added arguments for sum() to allow auto-binding with callbacks
* - Added arguments for avg() to allow auto-binding with callbacks
*
* v0.1a
* - Added semi-colons after object declaration (for min protection)
*
* v0.1
* - First public release
*
*/
(function($){
// set the defaults
var defaults = {
// regular expression used to detect numbers, if you want to force the field to contain
// numbers, you can add a ^ to the beginning or $ to the end of the regex to force the
// the regex to match the entire string: /^(-|-\$)?(\d+(,\d{3})*(\.\d{1,})?|\.\d{1,})$/g
reNumbers: /(-|-\$)?(\d+(,\d{3})*(\.\d{1,})?|\.\d{1,})/g
// this function is used in the parseNumber() to cleanse up any found numbers
// the function is intended to remove extra information found in a number such
// as extra commas and dollar signs. override this function to strip European values
, cleanseNumber: function (v){
// cleanse the number one more time to remove extra data (like commas and dollar signs)
// use this for European numbers: v.replace(/[^0-9,\-]/g, "").replace(/,/g, ".")
return v.replace(/[^0-9.\-]/g, "");
}
// should the Field plug-in be used for getting values of :input elements?
, useFieldPlugin: (!!$.fn.getValue)
// a callback function to run when an parsing error occurs
, onParseError: null
// a callback function to run once a parsing error has cleared
, onParseClear: null
};
// set default options
$.Calculation = {
version: "0.4.08",
setDefaults: function(options){
$.extend(defaults, options);
}
};
/*
* jQuery.fn.parseNumber()
*
* returns Array - detects the DOM element and returns it's value. input
* elements return the field value, other DOM objects
* return their text node
*
* NOTE: Breaks the jQuery chain, since it returns a Number.
*
* Examples:
* $("input[name^='price']").parseNumber();
* > This would return an array of potential number for every match in the selector
*
*/
// the parseNumber() method -- break the chain
$.fn.parseNumber = function(options){
var aValues = [];
options = $.extend(options, defaults);
this.each(
function (){
var
// get a pointer to the current element
$el = $(this),
// determine what method to get it's value
sMethod = ($el.is(":input") ? (defaults.useFieldPlugin ? "getValue" : "val") : "text"),
// parse the string and get the first number we find
v = $.trim($el[sMethod]()).match(defaults.reNumbers, "");
// if the value is null, use 0
if( v == null ){
v = 0; // update value
// if there's a error callback, execute it
if( jQuery.isFunction(options.onParseError) ) options.onParseError.apply($el, [sMethod]);
$.data($el[0], "calcParseError", true);
// otherwise we take the number we found and remove any commas
} else {
// clense the number one more time to remove extra data (like commas and dollar signs)
v = options.cleanseNumber.apply(this, [v[0]]);
// if there's a clear callback, execute it
if( $.data($el[0], "calcParseError") && jQuery.isFunction(options.onParseClear) ){
options.onParseClear.apply($el, [sMethod]);
// clear the error flag
$.data($el[0], "calcParseError", false);
}
}
aValues.push(parseFloat(v, 10));
}
);
// return an array of values
return aValues;
};
/*
* jQuery.fn.calc()
*
* returns Number - performance a calculation and updates the field
*
* Examples:
* $("input[name='price']").calc();
* > This would return the sum of all the fields named price
*
*/
// the calc() method
$.fn.calc = function(expr, vars, cbFormat, cbDone){
var
// create a pointer to the jQuery object
$this = this
// the value determine from the expression
, exprValue = ""
// track the precision to use
, precision = 0
// a pointer to the current jQuery element
, $el
// store an altered copy of the vars
, parsedVars = {}
// temp variable
, tmp
// the current method to use for updating the value
, sMethod
// a hash to store the local variables
, _
// track whether an error occured in the calculation
, bIsError = false;
// look for any jQuery objects and parse the results into numbers
for( var k in vars ){
// replace the keys in the expression
expr = expr.replace( (new RegExp("(" + k + ")", "g")), "_.$1");
if( !!vars[k] && !!vars[k].jquery ){
parsedVars[k] = vars[k].parseNumber();
} else {
parsedVars[k] = vars[k];
}
}
this.each(
function (i, el){
var p, len;
// get a pointer to the current element
$el = $(this);
// determine what method to get it's value
sMethod = ($el.is(":input") ? (defaults.useFieldPlugin ? "setValue" : "val") : "text");
// initialize the hash vars
_ = {};
for( var k in parsedVars ){
if( typeof parsedVars[k] == "number" ){
_[k] = parsedVars[k];
} else if( typeof parsedVars[k] == "string" ){
_[k] = parseFloat(parsedVars[k], 10);
} else if( !!parsedVars[k] && (parsedVars[k] instanceof Array) ) {
// if the length of the array is the same as number of objects in the jQuery
// object we're attaching to, use the matching array value, otherwise use the
// value from the first array item
tmp = (parsedVars[k].length == $this.length) ? i : 0;
_[k] = parsedVars[k][tmp];
}
// if we're not a number, make it 0
if( isNaN(_[k]) ) _[k] = 0;
// check for decimals and check the precision
p = _[k].toString().match(/\.\d+$/gi);
len = (p) ? p[0].length-1 : 0;
// track the highest level of precision
if( len > precision ) precision = len;
}
// try the calculation
try {
exprValue = eval( expr );
// fix any the precision errors
if( precision ) exprValue = Number(exprValue.toFixed(Math.max(precision, 4)));
// if there's a format callback, call it now
if( jQuery.isFunction(cbFormat) ){
// get return value
var tmp = cbFormat.apply(this, [exprValue]);
// if we have a returned value (it's null null) use it
if( !!tmp ) exprValue = tmp;
}
// if there's an error, capture the error output
} catch(e){
exprValue = e;
bIsError = true;
}
// update the value
$el[sMethod](exprValue.toString());
}
);
// if there's a format callback, call it now
if( jQuery.isFunction(cbDone) ) cbDone.apply(this, [this]);
return this;
};
/*
* Define all the core aggregate functions. All of the following methods
* have the same functionality, but they perform different aggregate
* functions.
*
* If this methods are called without any arguments, they will simple
* perform the specified aggregate function and return the value. This
* will break the jQuery chain.
*
* However, if you invoke the method with any arguments then a jQuery
* object is returned, which leaves the chain intact.
*
*
* jQuery.fn.sum()
* returns Number - the sum of all fields
*
* jQuery.fn.avg()
* returns Number - the avg of all fields
*
* jQuery.fn.min()
* returns Number - the minimum value in the field
*
* jQuery.fn.max()
* returns Number - the maximum value in the field
*
* Examples:
* $("input[name='price']").sum();
* > This would return the sum of all the fields named price
*
* $("input[name='price1'], input[name='price2'], input[name='price3']").sum();
* > This would return the sum of all the fields named price1, price2 or price3
*
* $("input[name^=sum]").sum("keyup", "#totalSum");
* > This would update the element with the id "totalSum" with the sum of all the
* > fields whose name started with "sum" anytime the keyup event is triggered on
* > those field.
*
* NOTE: The syntax above is valid for any of the aggregate functions
*
*/
$.each(["sum", "avg", "min", "max"], function (i, method){
$.fn[method] = function (bind, selector){
// if no arguments, then return the result of the aggregate function
if( arguments.length == 0 )
return math[method](this.parseNumber());
// if the selector is an options object, get the options
var bSelOpt = selector && (selector.constructor == Object) && !(selector instanceof jQuery);
// configure the options for this method
var opt = bind && bind.constructor == Object ? bind : {
bind: bind || "keyup"
, selector: (!bSelOpt) ? selector : null
, oncalc: null
};
// if the selector is an options object, extend the options
if( bSelOpt ) opt = jQuery.extend(opt, selector);
// if the selector exists, make sure it's a jQuery object
if( !!opt.selector ) opt.selector = $(opt.selector);
var self = this
, sMethod
, doCalc = function (){
// preform the aggregate function
var value = math[method](self.parseNumber(opt));
// check to make sure we have a selector
if( !!opt.selector ){
// determine how to set the value for the selector
sMethod = (opt.selector.is(":input") ? (defaults.useFieldPlugin ? "setValue" : "val") : "text");
// update the value
opt.selector[sMethod](value.toString());
}
// if there's a callback, run it now
if( jQuery.isFunction(opt.oncalc) ) opt.oncalc.apply(self, [value, opt]);
};
// perform the aggregate function now, to ensure init values are updated
doCalc();
// bind the doCalc function to run each time a key is pressed
return self.bind(opt.bind, doCalc);
}
});
/*
* Mathmatical functions
*/
var math = {
// sum an array
sum: function (a){
var total = 0, precision = 0;
// loop through the value and total them
$.each(a, function (i, v){
// check for decimals and check the precision
var p = v.toString().match(/\.\d+$/gi), len = (p) ? p[0].length-1 : 0;
// track the highest level of precision
if( len > precision ) precision = len;
// we add 0 to the value to ensure we get a numberic value
total += v;
});
// fix any the precision errors
if( precision ) total = Number(total.toFixed(precision));
// return the values as a comma-delimited string
return total;
},
// average an array
avg: function (a){
// return the values as a comma-delimited string
return math.sum(a)/a.length;
},
// lowest number in array
min: function (a){
return Math.min.apply(Math, a);
},
// highest number in array
max: function (a){
return Math.max.apply(Math, a);
}
};
})(jQuery);