/** * jquery.numberformatter - Formatting/Parsing Numbers in jQuery * * Written by * Michael Abernethy (mike@abernethysoft.com), * Andrew Parry (aparry0@gmail.com) * * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. * * @author Michael Abernethy, Andrew Parry * @version 1.2.3-SNAPSHOT ($Id$) * * Dependencies * * jQuery (http://jquery.com) * jshashtable (http://www.timdown.co.uk/jshashtable) * * Notes & Thanks * * many thanks to advweb.nanasi.jp for his bug fixes * jsHashtable is now used also, so thanks to the author for that excellent little class. * * This plugin can be used to format numbers as text and parse text as Numbers * Because we live in an international world, we cannot assume that everyone * uses "," to divide thousands, and "." as a decimal point. * * As of 1.2 the way this plugin works has changed slightly, parsing text to a number * has 1 set of functions, formatting a number to text has it's own. Before things * were a little confusing, so I wanted to separate the 2 out more. * * * jQuery extension functions: * * formatNumber(options, writeBack, giveReturnValue) - Reads the value from the subject, parses to * a Javascript Number object, then formats back to text using the passed options and write back to * the subject. * * parseNumber(options) - Parses the value in the subject to a Number object using the passed options * to decipher the actual number from the text, then writes the value as text back to the subject. * * * Generic functions: * * formatNumber(numberString, options) - Takes a plain number as a string (e.g. '1002.0123') and returns * a string of the given format options. * * parseNumber(numberString, options) - Takes a number as text that is formatted the same as the given * options then and returns it as a plain Number object. * * To achieve the old way of combining parsing and formatting to keep say a input field always formatted * to a given format after it has lost focus you'd simply use a combination of the functions. * * e.g. * $("#salary").blur(function(){ * $(this).parseNumber({format:"#,###.00", locale:"us"}); * $(this).formatNumber({format:"#,###.00", locale:"us"}); * }); * * The syntax for the formatting is: * 0 = Digit * # = Digit, zero shows as absent * . = Decimal separator * - = Negative sign * , = Grouping Separator * % = Percent (multiplies the number by 100) * * For example, a format of "#,###.00" and text of 4500.20 will * display as "4.500,20" with a locale of "de", and "4,500.20" with a locale of "us" * * * As of now, the only acceptable locales are * Arab Emirates -> "ae" * Australia -> "au" * Austria -> "at" * Brazil -> "br" * Canada -> "ca" * China -> "cn" * Czech -> "cz" * Denmark -> "dk" * Egypt -> "eg" * Finland -> "fi" * France -> "fr" * Germany -> "de" * Greece -> "gr" * Great Britain -> "gb" * Hong Kong -> "hk" * India -> "in" * Israel -> "il" * Japan -> "jp" * Russia -> "ru" * South Korea -> "kr" * Spain -> "es" * Sweden -> "se" * Switzerland -> "ch" * Taiwan -> "tw" * Thailand -> "th" * United States -> "us" * Vietnam -> "vn" **/ (function(jQuery) { var nfLocales = new Hashtable(); var nfLocalesLikeUS = [ 'ae','au','ca','cn','eg','gb','hk','il','in','jp','sk','th','tw','us' ]; var nfLocalesLikeDE = [ 'at','br','de','dk','es','gr','it','nl','pt','tr','vn' ]; var nfLocalesLikeFR = [ 'cz','fi','fr','ru','se','pl' ]; var nfLocalesLikeCH = [ 'ch' ]; var nfLocaleFormatting = [ [".", ","], [",", "."], [",", " "], [".", "'"] ]; var nfAllLocales = [ nfLocalesLikeUS, nfLocalesLikeDE, nfLocalesLikeFR, nfLocalesLikeCH ] function FormatData(dec, group, neg) { this.dec = dec; this.group = group; this.neg = neg; }; function init() { // write the arrays into the hashtable for (var localeGroupIdx = 0; localeGroupIdx < nfAllLocales.length; localeGroupIdx++) { localeGroup = nfAllLocales[localeGroupIdx]; for (var i = 0; i < localeGroup.length; i++) { nfLocales.put(localeGroup[i], localeGroupIdx); } } }; function formatCodes(locale, isFullLocale) { if (nfLocales.size() == 0) init(); // default values var dec = "."; var group = ","; var neg = "-"; if (isFullLocale == false) { // Extract and convert to lower-case any language code from a real 'locale' formatted string, if not use as-is // (To prevent locale format like : "fr_FR", "en_US", "de_DE", "fr_FR", "en-US", "de-DE") if (locale.indexOf('_') != -1) locale = locale.split('_')[1].toLowerCase(); else if (locale.indexOf('-') != -1) locale = locale.split('-')[1].toLowerCase(); } // hashtable lookup to match locale with codes var codesIndex = nfLocales.get(locale); if (codesIndex) { var codes = nfLocaleFormatting[codesIndex]; if (codes) { dec = codes[0]; group = codes[1]; } } return new FormatData(dec, group, neg); }; /* Formatting Methods */ /** * Formats anything containing a number in standard js number notation. * * @param {Object} options The formatting options to use * @param {Boolean} writeBack (true) If the output value should be written back to the subject * @param {Boolean} giveReturnValue (true) If the function should return the output string */ jQuery.fn.formatNumber = function(options, writeBack, giveReturnValue) { return this.each(function() { // enforce defaults if (writeBack == null) writeBack = true; if (giveReturnValue == null) giveReturnValue = true; // get text var text; if (jQuery(this).is(":input")) text = new String(jQuery(this).val()); else text = new String(jQuery(this).text()); // format var returnString = jQuery.formatNumber(text, options); // set formatted string back, only if a success // if (returnString) { if (writeBack) { if (jQuery(this).is(":input")) jQuery(this).val(returnString); else jQuery(this).text(returnString); } if (giveReturnValue) return returnString; // } // return ''; }); }; /** * First parses a string and reformats it with the given options. * * @param {Object} numberString * @param {Object} options */ jQuery.formatNumber = function(numberString, options){ var options = jQuery.extend({}, jQuery.fn.formatNumber.defaults, options); var formatData = formatCodes(options.locale.toLowerCase(), options.isFullLocale); var dec = formatData.dec; var group = formatData.group; var neg = formatData.neg; var validFormat = "0#-,."; // strip all the invalid characters at the beginning and the end // of the format, and we'll stick them back on at the end // make a special case for the negative sign "-" though, so // we can have formats like -$23.32 var prefix = ""; var negativeInFront = false; for (var i = 0; i < options.format.length; i++) { if (validFormat.indexOf(options.format.charAt(i)) == -1) prefix = prefix + options.format.charAt(i); else if (i == 0 && options.format.charAt(i) == '-') { negativeInFront = true; continue; } else break; } var suffix = ""; for (var i = options.format.length - 1; i >= 0; i--) { if (validFormat.indexOf(options.format.charAt(i)) == -1) suffix = options.format.charAt(i) + suffix; else break; } options.format = options.format.substring(prefix.length); options.format = options.format.substring(0, options.format.length - suffix.length); // now we need to convert it into a number //while (numberString.indexOf(group) > -1) // numberString = numberString.replace(group, ''); //var number = new Number(numberString.replace(dec, ".").replace(neg, "-")); var number = new Number(numberString); return jQuery._formatNumber(number, options, suffix, prefix, negativeInFront); }; /** * Formats a Number object into a string, using the given formatting options * * @param {Object} numberString * @param {Object} options */ jQuery._formatNumber = function(number, options, suffix, prefix, negativeInFront) { var options = jQuery.extend({}, jQuery.fn.formatNumber.defaults, options); var formatData = formatCodes(options.locale.toLowerCase(), options.isFullLocale); var dec = formatData.dec; var group = formatData.group; var neg = formatData.neg; var forcedToZero = false; if (isNaN(number)) { if (options.nanForceZero == true) { number = 0; forcedToZero = true; } else return null; } // special case for percentages if (suffix == "%") number = number * 100; var returnString = ""; if (options.format.indexOf(".") > -1) { var decimalPortion = dec; var decimalFormat = options.format.substring(options.format.lastIndexOf(".") + 1); // round or truncate number as needed if (options.round == true) number = new Number(number.toFixed(decimalFormat.length)); else { var numStr = number.toString(); numStr = numStr.substring(0, numStr.lastIndexOf('.') + decimalFormat.length + 1); number = new Number(numStr); } var decimalValue = number % 1; var decimalString = new String(decimalValue.toFixed(decimalFormat.length)); decimalString = decimalString.substring(decimalString.lastIndexOf(".") + 1); for (var i = 0; i < decimalFormat.length; i++) { if (decimalFormat.charAt(i) == '#' && decimalString.charAt(i) != '0') { decimalPortion += decimalString.charAt(i); continue; } else if (decimalFormat.charAt(i) == '#' && decimalString.charAt(i) == '0') { var notParsed = decimalString.substring(i); if (notParsed.match('[1-9]')) { decimalPortion += decimalString.charAt(i); continue; } else break; } else if (decimalFormat.charAt(i) == "0") decimalPortion += decimalString.charAt(i); } returnString += decimalPortion } else number = Math.round(number); var ones = Math.floor(number); if (number < 0) ones = Math.ceil(number); var onesFormat = ""; if (options.format.indexOf(".") == -1) onesFormat = options.format; else onesFormat = options.format.substring(0, options.format.indexOf(".")); var onePortion = ""; if (!(ones == 0 && onesFormat.substr(onesFormat.length - 1) == '#') || forcedToZero) { // find how many digits are in the group var oneText = new String(Math.abs(ones)); var groupLength = 9999; if (onesFormat.lastIndexOf(",") != -1) groupLength = onesFormat.length - onesFormat.lastIndexOf(",") - 1; var groupCount = 0; for (var i = oneText.length - 1; i > -1; i--) { onePortion = oneText.charAt(i) + onePortion; groupCount++; if (groupCount == groupLength && i != 0) { onePortion = group + onePortion; groupCount = 0; } } // account for any pre-data padding if (onesFormat.length > onePortion.length) { var padStart = onesFormat.indexOf('0'); if (padStart != -1) { var padLen = onesFormat.length - padStart; // pad to left with 0's or group char var pos = onesFormat.length - onePortion.length - 1; while (onePortion.length < padLen) { var padChar = onesFormat.charAt(pos); // replace with real group char if needed if (padChar == ',') padChar = group; onePortion = padChar + onePortion; pos--; } } } } if (!onePortion && onesFormat.indexOf('0', onesFormat.length - 1) !== -1) onePortion = '0'; returnString = onePortion + returnString; // handle special case where negative is in front of the invalid characters if (number < 0 && negativeInFront && prefix.length > 0) prefix = neg + prefix; else if (number < 0) returnString = neg + returnString; if (!options.decimalSeparatorAlwaysShown) { if (returnString.lastIndexOf(dec) == returnString.length - 1) { returnString = returnString.substring(0, returnString.length - 1); } } returnString = prefix + returnString + suffix; return returnString; }; /* Parsing Methods */ /** * Parses a number of given format from the element and returns a Number object. * @param {Object} options */ jQuery.fn.parseNumber = function(options, writeBack, giveReturnValue) { // enforce defaults if (writeBack == null) writeBack = true; if (giveReturnValue == null) giveReturnValue = true; // get text var text; if (jQuery(this).is(":input")) text = new String(jQuery(this).val()); else text = new String(jQuery(this).text()); // parse text var number = jQuery.parseNumber(text, options); if (number) { if (writeBack) { if (jQuery(this).is(":input")) jQuery(this).val(number.toString()); else jQuery(this).text(number.toString()); } if (giveReturnValue) return number; } }; /** * Parses a string of given format into a Number object. * * @param {Object} string * @param {Object} options */ jQuery.parseNumber = function(numberString, options) { var options = jQuery.extend({}, jQuery.fn.parseNumber.defaults, options); var formatData = formatCodes(options.locale.toLowerCase(), options.isFullLocale); var dec = formatData.dec; var group = formatData.group; var neg = formatData.neg; var valid = "1234567890.-"; // now we need to convert it into a number while (numberString.indexOf(group)>-1) numberString = numberString.replace(group,''); numberString = numberString.replace(dec,".").replace(neg,"-"); var validText = ""; var hasPercent = false; if (numberString.charAt(numberString.length - 1) == "%" || options.isPercentage == true) hasPercent = true; for (var i=0; i-1) validText = validText + numberString.charAt(i); } var number = new Number(validText); if (hasPercent) { number = number / 100; var decimalPos = validText.indexOf('.'); if (decimalPos != -1) { var decimalPoints = validText.length - decimalPos - 1; number = number.toFixed(decimalPoints + 2); } else { number = number.toFixed(validText.length - 1); } } return number; }; jQuery.fn.parseNumber.defaults = { locale: "us", decimalSeparatorAlwaysShown: false, isPercentage: false, isFullLocale: false }; jQuery.fn.formatNumber.defaults = { format: "#,###.00", locale: "us", decimalSeparatorAlwaysShown: false, nanForceZero: true, round: true, isFullLocale: false }; Number.prototype.toFixed = function(precision) { return jQuery._roundNumber(this, precision); }; jQuery._roundNumber = function(number, decimalPlaces) { var power = Math.pow(10, decimalPlaces || 0); var value = String(Math.round(number * power) / power); // ensure the decimal places are there if (decimalPlaces > 0) { var dp = value.indexOf("."); if (dp == -1) { value += '.'; dp = 0; } else { dp = value.length - (dp + 1); } while (dp < decimalPlaces) { value += '0'; dp++; } } return value; }; })(jQuery);