Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

JavaScript Number Parsing and Formatting for Multicultural Environments

, 21 Dec 2010 CPOL
A JavaScript class used to help in formatting and parsing numbers when parseInt and parseFloat are not enough
//Filename : NumberFormat.js
//NumberFormatter V1.0

//Author: Luc Veronneau 
//Email: hurlemonde@hotmail.com

//This script is release under The Code Project Open License (CPOL) 
//see: http://www.codeproject.com/info/cpol10.aspx
//Under no circomstances remove or modify this header

(function () {

    //Declaring namespace
    var _formatting = window.Formatting = {};
    _formatting.__namespace = true;

    //Enum representing negative patterns used by .net
    var _numberNegativePattern = _formatting.NumberNegativePattern = {
        //Negative is reprensented by enclosing parentheses ex: (1500) corresponds to -1500
        Pattern0: 0,
        //Negative is represented by leading "-"
        Pattern1: 1,
        //Negative is represented by leading "- "
        Pattern2: 2,
        //Negative is represented by following "-"
        Pattern3: 3,
        //Negative is represented by following " -"
        Pattern4: 4
    };

    var _numberFormatInfo = _formatting.NumberFormatInfo = function () {
        ///<summary>Information class passed to the NumberFormat class to be used to format text for numbers properly</summary>
        ///<returns type="Formatting.NumberFormatInfo" />
        if (arguments.length === 1) {
            for (var item in this) {
                if (typeof this[item] != "function") {
                    if (typeof this[item] != typeof arguments[0][item])
                        throw "Argument does not match NumberFormatInfo";
                }
            }
            return arguments[0];
        }
    };

    _numberFormatInfo.prototype = {
        //Negative sign property
        NegativeSign: "-",
        //Default number of digits used by the numberformat
        NumberDecimalDigits: 2,
        //Seperator used to seperate digits from integers
        NumberDecimalSeparator: ".",
        //Seperator used to split integer groups (ex: official US formatting of a number is 1,150.50 where "," if the group seperator)
        NumberGroupSeparator: ",",
        //Group sizes originally an array in .net but normally groups numbers are either by 3 or not grouped at all
        NumberGroupSizes: 3,
        //Negative patterns used by .net
        NumberNegativePattern: Formatting.NumberNegativePattern.Pattern1
    };
    _numberFormatInfo.__class = true;
})();

(function () {

    //Main constructor for the NumberFormatter
    var _numberFormatter = window.Formatting.NumberFormatter = function (formatInfo) {
        ///<summary> Manages number formatting using format infos </summary>
        ///<param name="formatInfo" type="Formatting.NumberFormatInfo" />
        this.FormatInfo = formatInfo;

        var groupSeperatorReg = this.GetRegexPartForChar(this.FormatInfo.NumberGroupSeparator);
        this.GroupSeperatorReg = new RegExp(groupSeperatorReg,"g");

        var decimalSeperator = this.GetRegexPartForChar(this.FormatInfo.NumberDecimalSeparator);
        this.DecimalSeperatorReg = new RegExp(decimalSeperator,"g");

        //Creating regex from the format info to validate input text
        var str = "";
        if (this.FormatInfo.NumberGroupSeparator != null
            && this.FormatInfo.NumberGroupSeparator.length > 0) {
            //The group seperator regex must take into account the possibility of having incomplete groups at the beginning 
            //once complete, this regex part should look like (\d{1,3}){0,1}
            str += "(\\d{1," + this.FormatInfo.NumberGroupSizes.toString() + "}){0,1}";
            str += "(" + groupSeperatorReg + "\\d{" + this.FormatInfo.NumberGroupSizes.toString() + "}){0,}";
        }
        else {
            str += "(\\d+)";
        }
        str += "(" + decimalSeperator + "\\d+)?";
        this.BaseRegexText = str;
        str = "^" + str + "$";
        this.NumberTester = new RegExp(str);

    };

    //Prototype for the NumberFormatter class
    _numberFormatter.prototype = {
        //FormatInfo property containing localized number informations
        FormatInfo: new Formatting.NumberFormatInfo(),
        //Regex used to validate text prior to parsing
        NumberTester: new RegExp(),
        //Base regex used to concatenate with other regex
        BaseRegexText: "",
        //Regex used to find decimal seperator
        DecimalSeperatorReg : new RegExp(),
        //Regex used to find group seperator
        GroupSeperatorReg : new RegExp(),
        Parse: function (value) {
            ///<summary>Parses a string and converts it to numeric, throws if the format is wrong</summary>
            ///<param name="value" type="string" />
            ///<returns type="Number" />
            return this.TryParse(value, function (errormessage, val) {
                throw errormessage + "ArgumentValue:" + val;
            });
        },
        TryParse: function (value, parseFailure) {
            ///<summary>Parses a string and converts it to numeric and calls a method if validation fails</summary>
            ///<param name="value" type="string">The value to parse</param>
            ///<param name="parseFailure" type="function">A function(ErrorMessage, parsedValue) delegate to call if the string does not respect the format</param>
            ///<returns type="Number" />

            var isNegative = this.GetNegativeRegex().test(value);
            var val = value;
            if (isNegative)
                val = this.GetNegativeRegex().exec(value)[1];

            if (!this.NumberTester.test(val)) {
                parseFailure("The number passed as argument does not respect the correct culture format.", val);
                return null;
            }

            var matches = this.NumberTester.exec(val);
            var decLen = matches[matches.length - 1].length - 1;

            var partial = val.replace(this.GroupSeperatorReg, "").replace(this.DecimalSeperatorReg, "");

            if (isNegative)
                partial = "-" + partial;
            
            return (parseInt(partial) / (Math.pow(10,decLen)));
        },
        ToString: function (value) {
            ///<summary>Converts a number to string</summary>
            ///<param name="value" type="Number" />
            ///<returns type="String" />
            var result = "";
            var isNegative = false;
            if (value < 0)
                isNegative = true;

            var baseString = value.toString();
            //Remove the default negative sign
            baseString = baseString.replace("-", "");

            //Split digits from integers
            var values = baseString.split(".");

            //Fetch integers and digits
            var ints = values[0];
            var digits = "";
            if (values.length > 1)
                digits = values[1];

            //Format the left part of the number according to grouping char and size
            if (this.FormatInfo.NumberGroupSeparator != null
                && this.FormatInfo.NumberGroupSeparator.length > 0) {

                //Verifying if a first partial group is present
                var startLen = ints.length % this.FormatInfo.NumberGroupSizes;
                if (startLen == 0 && ints.length > 0)
                    startLen = this.FormatInfo.NumberGroupSizes;
                //Fetching the total number of groups
                var numberOfGroups = Math.ceil(ints.length / this.FormatInfo.NumberGroupSizes);
                //If only one, juste assign the value 
                if (numberOfGroups == 1) {
                    result += ints;
                }
                else {
                    // More than one group
                    //If a startlength is present, assign it so the rest of the string is a multiple of the group size
                    if (startLen > 0) {
                        result += ints.substring(0, startLen);
                        ints = ints.slice(-(ints.length - startLen));
                    }
                    //Group up the rest of the integers into their full groups
                    while (ints.length > 0) {
                        result += this.FormatInfo.NumberGroupSeparator + ints.substring(0, this.FormatInfo.NumberGroupSizes);
                        if (ints.length == this.FormatInfo.NumberGroupSizes)
                            break;
                        ints = ints.slice(-(ints.length - this.FormatInfo.NumberGroupSizes));
                    }
                }
            }
            else
                result += ints; //Left part is not grouped

            //If digits are present, concatenate them
            if (digits.length > 0)
                result += this.FormatInfo.NumberDecimalSeparator + digits;

            //If number is negative, decorate the number with the negative sign
            if (isNegative)
                result = this.FormatNegative(result);

            return result;
        },
        GetRegexPartForChar: function (part) {
            switch (part.charCodeAt(0)) {
                case 160:
                case 32:
                    return "[\\s\\xa0]";
                case 46:
                    return "[\\.]";
                default:
                    return "[" + part + "]";
            }
        },
        GetNegativeRegex: function () {
            ///<summary>Method creating a regex used to test if a number is negative or not</summary>
            ///<returns type="RegExp" />
            switch (this.FormatInfo.NumberNegativePattern) {
                case 0:
                    return new RegExp("^[(](" + this.BaseRegexText + ")[)]$");
                case 1:
                    return new RegExp("^" + this.FormatInfo.NegativeSign + "(" + this.BaseRegexText + ")$");
                case 2:
                    return new RegExp("^[" + this.FormatInfo.NegativeSign + " ](" + this.BaseRegexText + ")$");
                case 3:
                    return new RegExp("^(" + this.BaseRegexText + ")[-]$");
                case 4:
                    return new RegExp("^(" + this.BaseRegexText + ")[\s][-]$");
                default:
                    return null;
            }
        },
        FormatNegative: function (numberString) {
            ///<summary>Method used to format an unsigned string into un negative localized number</summary>
            ///<returns type="String" />
            switch (this.FormatInfo.NumberNegativePattern) {
                case 0:
                    return "(" + numberString + ")";
                case 1:
                    return this.FormatInfo.NegativeSign + numberString;
                case 2:
                    return this.FormatInfo.NegativeSign + " " + numberString;
                case 3:
                    return numberString + "-";
                case 4:
                    return numberString + " -";
                default:
                    return null;
            }
        }
    };
    _numberFormatter.__class = true;
})();

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Luc Veronneau
Web Developer Faltour Inc.
Canada Canada
From the beginning i have always been passionate about everything. I have never actually studied in computer programming but life brought me in IT departments from the beginning of my career. The more i programmed the more i liked it. Ever since, i've been hooked.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141216.1 | Last Updated 21 Dec 2010
Article Copyright 2010 by Luc Veronneau
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid