Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

JavaScript Number Parsing and Formatting for Multicultural Environments

5.00/5 (3 votes)
21 Dec 2010CPOL6 min read 36.6K   505  
A JavaScript class used to help in formatting and parsing numbers when parseInt and parseFloat are not enough

Introduction

The purpose of this article is to help JavaScript number conversion to and from strings thus allowing the UI to respect the .NET CultureInfo.NumberFormatInfo selected by the user.

Background

Ever since I started working in web development, I have had to deal with a normally bilingual community. You see, I come from Montreal and most people speak French, many English and obviously some people have a foreign first language. Now most speak French and / or English and each has his/her own preference when it comes to language (which is not always the first language). That means that all web applications must (by law anyway) be bilingual. The worst part of all this has (almost) always been numbers. Now most people don't know how to format a number properly but that's beside the point. There already are many plug-ins out there to solve the data entry problem. The value conversion problem though has often been an issue since number formatting in JavaScript and .NET is different. When the need to receive native values from .NET in the form of DataContracts directly in JavaScript arose, there was no choice but to use some kind of converter to format and parse numeric values according to the specifics of the region and the language, hence I decided to create a number formatter based on the .NET NumberFormatInfo class. You can find the JavaScript code itself in the NumberFormat.zip download and a short example on how to use it.

Now with the Class

Before we begin with the classes, I suggest you download if not the project at least the JS file itself as it contains more code and will help in understanding the whole issue.

NumberFormatter class is contained within a namespace:

JavaScript
var _formatting = window.Formatting = {};
_formatting.__namespace = true; 

This namespace contains one enum and two classes.

The Enum:

JavaScript
//Enum representing negative patterns used by .net
    var _numberNegativePattern = _formatting.NumberNegativePattern = {
        //Negative is represented 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
    };

This enum is accessed through Formatting.NumberNegativePattern. Its use is to ease access and understanding of the .NET negative pattern which can change depending on the context on which the number is used. For example, an accountant will often use the parentheses as a means to express a negative number (or amounts) within account summaries. (example: $ (1200.00) reads minus 1200 dollars) So if one would want to be able to read and write forms elements for this accountant, they would use Formatting.NumberNegativePattern.Pattern0. The standard is to use Pattern1 as it corresponds to a dash written directly before the first integer of the number.

Now the classes:

JavaScript
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,
        //Separator used to separate digits from integers
        NumberDecimalSeparator: ".",
        //Separator used to split integer groups (ex: official US formatting 
        //of a number is 1,150.50 where "," if the group separator)
        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;

Ok if you've looked into .NET Globalization namespace, you probably recognize this one as a partial NumberFormatInfo which is precisely what it is. It's used as a configuration object fed to the constructor of Formatting.NumberFormatter. Nothing special here except a couple of differences from the .NET class, for example NumberGroupSizes which for simplification purposes is a field instead of an array. I will probably change that one back to array as it will give more freedom for number formatting. The NumberNegativePattern here corresponds to an enum value as opposed to its .NET counterpart which is an int. OK my enum is just int values, I know, but still it helps make things clearer by putting words where there aren't any.

Now I won't show the whole NumberFormatter class here because it would stretch on forever. I will explain the main methods.

  • Parse
  • TryParse
  • ToString
JavaScript
Parse: function (value) {
    ///<summary>Parses a string and converts it to numeric, 
    ///throws an exception 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;
} 

As you have seen, parse merely wraps TryParse so I'll go on to explaining the TryParse method itself. Basically, the first part tests if the value corresponds to the negative pattern. The negative pattern method wraps a regex that tests for the pattern around the NumberTester regex. This enables to test negativeness of the number and to return the absolute value that corresponds to the number. Now if the number format is wrong, the negative test will return false which in turn passes the whole value to the number tester which fails and sends the error message back to the caller. In the case when the format is right, we need to take a look at the decimals to see how many there are so that we can parse an integer and then correct the decimal place. This is done because some browsers have a hard time with parseFloat which doesn't always return the precise value. Next, we replace group separator and the decimal separator with empty strings. Now since we already extracted the absolute value from the negative pattern, this makes the string a valid positive integer number. If it was a negative number, we just concatenate with dash. Finally, to have the exact value sent back to the client, we just have to parse the integer and divide by ten to the power of the number of decimals. We could have written the string representing the value with decimals and then call eval on it, that would have sent back the same value. Now eval is awfully slower and I use it the least possible so I found another way to go. I could have used return new Number("numberString") which seemed pretty much as fast but I heard about people having trouble with decimals using Number object too. The parseInt solution seemed the safest to me.

ToString is pretty self explanatory. I guess I might have been able to optimize the whole grouping part but it seemed fine like that. Basically, we take a number value, call tostring, split the decimals and format the integers into groups after adding the decimals again with the right decimal separator. After all this, if the number was negative, we decorate the number with the negative sign and return the resulting string.

Using the Code

Now that we covered the way the main methods are built, let's see how we can make intelligent use of that. First here's the simplest way to use it:

JavaScript
var formatter = new Formatting.NumberFormatter(new Formatting.NumberFormatInfo());
$(thisControlSelector).val(formatter.ToString(123456.789));
var value = formatter.Parse($(thisControlSelector).val());

This would create a basic formatter using the default NumberFormatInfo values (US Format). From that, the number would be written in the value of the control as "123,456.789" and then it would be parsed back into the variable "value". You will find in the example project a way to use the server side CultureInfo data and send it back to client side using data contracts that can be used by the NumberFormatter directly. Now if one wanted to use specialized number formats for example to parse and write numbers that are used in accounting reports, one would just have to define his/her own specialized NumberFormatInfo class which in turn can be used server side and sent back to the client side to be used by NumberFormatter as well. To enable intellisense and validate that all properties needed are present, one would create an instance of the client side NumberFormatInfo passing the return value of the web service as an argument as done in the example code below.

JavaScript
function call() {
            var settings = $.extend({}, $.ajaxSettings);
            settings.contentType = 'application/json; charset=utf-8';
            settings.dataType = "json";
            settings.async = false;
            settings.type = "POST";
            settings.url = "FormattingServices.svc/GetFormat";
            settings.data = JSON.stringify({ format: _formatName.val() });
            //settings.processData = false;
            settings.cache = false;
            settings.success = function (returnObject) {
                testFormat(new Formatting.NumberFormatter
		(new Formatting.NumberFormatInfo(returnObject.GetFormatResult)));
            }
            settings.error = function (XMLHttpRequest, textStatus, errorThrown) {
                alert(errorThrown);
            }
            $.ajax(settings);
        }
function testFormat(formatter) {
            ///<summary>Method used to test formatters</summary>
            ///<param name="formatter" type="Formatting.NumberFormatter">
            ///The formatter to use</param>
            _formatter = formatter;
            var value = parseFloat(_testValue.val());

            _formatted.val(formatter.ToString(value));

            value = formatter.TryParse(_formatted.val(), function 
				(errorMessage, parsedValue) {
                alert(errorMessage);
            });

            _unformatted.val(value.toString());
        }

Conclusion

We have seen how wrapping up Number parsing and writing can simplify the life of the programmer when programming client side logic. It has become more and more popular in professional websites to program entirely async pages and make them as independent from the server as possible. With this kind of class, now one only has to access web services (for instance REST services) to fetch data and save data without having to depend on the web server for number formatting purposes. Obviously, this class alone is not enough and I will for sure add other ones. Next in line is dates, for which, just like numbers, we have plenty of date pickers but mostly no specialized formatters enabling us to read and write localized dates.

License

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