/*
 * 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: 11
 * Version: 0.4.07
 *
 * Revision History
 * 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
 *
 
 
 
$("input[name^='price']").parseNumber();
This would return an array of potential number for every match in the selector 
$("input[name^='sum']").sum();
Returns the sum of all the input objects that start with a name attribute of "sum". This breaks the jQuery chain. 
$("input[name^=';sum']").sum("keyup", "#totalSum");
Updates the "#totalSum" element with the sum of all the input objects that start with the name attribute of "sum" anytime the keyup event occurs within those fields. This does not break the jQuery chain. 
$("input[name^='avg']").avg();
Returns the average of all the input objects that start with a name attribute of "avg". 
$("input[name^='avg']").avg("keyup", "#totalAvg");
Updates the "#totalAvg" element with the average of all the input objects that start with the name attribute of "avg" anytime the keyup event occurs within those fields. This does not break the jQuery chain. 
$("input[name^='avg']").avg({
	  bind: "keyup"
	, selector: "#totalAvg"
	, oncalc: function (value, settings){
		// you can use this callback to format values
		$(settings.selector).html("$" + value);
	}
});
Updates the "#totalAvg" element with the average of all the input objects that start with the name attribute of "avg" anytime the keyup event occurs within those fields. This uses the oncalc callback to format the results by appending a "$" to front of the value. This does not break the jQuery chain. 
$("input[name^='min']").min();
Returns the minimum value of all the input objects that start with a name attribute of "min". 
$("input[name^='max']").max();
Returns the maximum value of all the input objects that start with a name attribute of "max". 
$("[id^=total_item]").calc(
	// the equation to use for the calculation
	"qty * price",
	// define the variables used in the equation, these can be a jQuery object
	{
		qty: $("input[name^=qty_item_]"),
		price: $("[id^=price_item_]")
	},
	// define the formatting callback, the results of the calculation are passed to this function
	function (s){
		// return the number as a dollar amount
		return "$" + s.toFixed(2);
	},
	// define the finish callback, this runs after the calculation has been complete
	function ($this){
		// sum the total of the $("[id^=total_item]") selector
		var sum = $this.sum();
		
		$("#grandTotal").text(
			// round the results to 2 digits
			"$" + sum.toFixed(2)
		);
	}
);
This example shows off the code used quantity * price = total example shown above. 
$.Calculation.setDefaults({
	// 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
	// should the Field plug-in be used for getting values of :input elements?
	, useFieldPlugin: true/false
	// 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
});
Use the setDefaults() method to change the default parameters for the Calculation Plug-in. If the Field Plug-in is loaded, then it will be used by default.

For european formatting (i.e. 1.000,00) set the following defaults:
$.Calculation.setDefaults({
	// a regular expression for detecting European-style formatted numbers
	reNumbers: /(-|-\$)?(\d+(\.\d{3})*(,\d{1,})?|,\d{1,})?/g
	// define a procedure to convert the string number into an actual usable number
	, 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, "").replace(/,/g, ".");
	}
})
 
 
 
*/
(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, "").replace(/,/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.07",
		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);


$.Calculation.setDefaults({
	// a regular expression for detecting European-style formatted numbers
	reNumbers: /(-|-\$)?(\d+(\.\d{3})*(,\d{1,})?|,\d{1,})?/g
	// define a procedure to convert the string number into an actual usable number
	, 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, "").replace(/,/g, ".");
	}
})
