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. 

Now with the class

 NumberFormatter class is contained within a namespace:

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

 This namespace contains one enum and two classes.

  The Enum:

//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
    };

 This enum is accessed through Formatting.NumberNegativePattern. It's 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 the express a negative number (or amounts) within account summaries. (ex: $ (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:

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;

 Ok if you've looked into .NET Globalization namespace you probably recognize this one as a partial NumberFormatInfo which is what it precisely 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 numberformatting. The NumberNegativePattern here corresponds to an enum value as opposed to it's .net counter part which is an int. OK 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
        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;
        } 

  As you have seen, parse merely wraps TryParse so I'll go on to explaining the TryParse method its-self.  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 'negativness' 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 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 seperator. 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, lets see how we can make intelligent use of that. First here's the simplest way to use it:

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, on 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.

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 obviously 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.