JavaScript Number Parsing and Formatting for Multicultural Environments





5.00/5 (3 votes)
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 string
s 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 DataContract
s 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:
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 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:
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
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 string
s. 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:
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.
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.