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
:
var _numberNegativePattern = _formatting.NumberNegativePattern = {
Pattern0: 0,
Pattern1: 1,
Pattern2: 2,
Pattern3: 3,
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 () {
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 = {
NegativeSign: "-",
NumberDecimalDigits: 2,
NumberDecimalSeparator: ".",
NumberGroupSeparator: ",",
NumberGroupSizes: 3,
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: function (value) {
return this.TryParse(value, function (errormessage, val) {
throw errormessage + "ArgumentValue:" + val;
});
},
TryParse: function (value, parseFailure) {
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) {
var result = "";
var isNegative = false;
if (value < 0)
isNegative = true;
var baseString = value.toString();
baseString = baseString.replace("-", "");
var values = baseString.split(".");
var ints = values[0];
var digits = "";
if (values.length > 1)
digits = values[1];
if (this.FormatInfo.NumberGroupSeparator != null
&& this.FormatInfo.NumberGroupSeparator.length > 0) {
var startLen = ints.length % this.FormatInfo.NumberGroupSizes;
if (startLen == 0 && ints.length > 0)
startLen = this.FormatInfo.NumberGroupSizes;
var numberOfGroups = Math.ceil(ints.length / this.FormatInfo.NumberGroupSizes);
if (numberOfGroups == 1) {
result += ints;
}
else {
if (startLen > 0) {
result += ints.substring(0, startLen);
ints = ints.slice(-(ints.length - startLen));
}
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;
if (digits.length > 0)
result += this.FormatInfo.NumberDecimalSeparator + digits;
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.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) {
_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.