JavaScript 多文化环境下的数字解析与格式化





5.00/5 (3投票s)
一个 JavaScript 类,用于在 parseInt 和 parseFloat 不够用时帮助格式化和解析数字
引言
本文的目的是帮助 JavaScript 将数字与string
进行相互转换,从而使 UI 能够尊重用户选择的 .NET CultureInfo.NumberFormatInfo
。
背景
自从我开始从事 Web 开发以来,我不得不处理一个通常是双语的社区。你看,我来自蒙特利尔,大多数人说法语,很多人说英语,当然也有一些人以其他语言为母语。现在大多数人说法语和/或英语,每个人在语言方面都有自己的偏好(不一定是母语)。这意味着所有 Web 应用程序都必须(无论如何根据法律)是双语的。这其中最糟糕的部分(几乎)一直都是数字。现在大多数人不知道如何正确格式化数字,但这已无关紧要。市面上已经有很多插件可以解决数据输入问题。然而,值转换问题常常是一个问题,因为 JavaScript 和 .NET 中的数字格式化是不同的。当需要将 .NET 中的原生值以DataContract
的形式直接在 JavaScript 中接收时,别无选择,只能使用某种转换器来根据区域和语言的特定要求来格式化和解析数值,因此我决定创建一个基于 .NET NumberFormatInfo
类的数字格式化器。您可以在 NumberFormat.zip 下载中找到 JavaScript 代码本身,以及一个关于如何使用它的 简短示例。
现在使用类
在我们开始介绍类之前,我建议您下载(即使不是整个项目,至少也要下载 JS 文件本身),因为它包含了更多的代码,有助于理解整个问题。
NumberFormatter
类包含在一个命名空间中
var _formatting = window.Formatting = {};
_formatting.__namespace = true;
此命名空间包含一个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
};
这个enum
可以通过Formatting.NumberNegativePattern
访问。它的目的是为了方便访问和理解 .NET 的负数模式,该模式会根据数字使用的上下文而变化。例如,会计师经常使用括号来表示账户汇总中的负数(或金额)。(例如:$ (1200.00) 表示零下 1200 美元)所以,如果想要能够读取和编写给这位会计师的表单元素,他们就会使用Formatting.NumberNegativePattern.Pattern0
。标准用法是Pattern1
,因为它对应于直接写在数字整数部分前面的破折号。
现在是类
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;
好的,如果您查看过 .NET Globalization 命名空间,您很可能认出这个类,它是一个部分NumberFormatInfo
,这正是它。它被用作一个配置对象,传递给Formatting.NumberFormatter
的构造函数。这里没有什么特别的,除了与 .NET 类有几个不同之处,例如NumberGroupSizes
,为了简化起见,它是一个字段而不是一个数组。我可能会将其改回数组,因为它将为数字格式化提供更多自由。这里的NumberNegativePattern
对应于一个enum
值,而不是 .NET 中对应的int
。好吧,我的enum
只是int
值,我知道,但它通过在没有单词的地方加上单词来帮助事情变得更清晰。
现在我不会在这里展示整个NumberFormatter
类,因为它会太长。我将解释主要方法。
解析
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;
}
正如您所见,parse 仅仅是 TryParse 的包装,所以我将继续解释TryParse
方法本身。基本上,第一部分测试值是否符合负数模式。负数模式方法包装了一个正则表达式,该正则表达式用于测试NumberTester
正则表达式周围的模式。这使得能够测试数字的负数性并返回与数字对应的绝对值。现在,如果数字格式错误,负数测试将返回false
,进而将整个值传递给数字测试器,数字测试器会失败并将错误消息发送回调用者。在格式正确的情况下,我们需要查看小数,看看有多少位小数,以便解析整数,然后纠正小数位数。这样做是因为一些浏览器在处理parseFloat
时存在困难,它并不总是返回精确的值。接下来,我们将分组分隔符和十进制分隔符替换为空string
。现在,由于我们已经从负数模式中提取了绝对值,这使得string
成为一个有效的正整数。如果它是负数,我们只需用破折号连接。最后,为了将确切的值发送回客户端,我们只需要解析整数并除以十的数字小数次幂。我们可以写出带小数的值的string
,然后对其调用eval
,这将返回相同的值。现在eval
慢得离谱,我尽量少用它,所以我找到了另一种方法。我可以使用return new Number("numberString")
,它看起来速度差不多,但我听说有人在使用Number
对象时也会遇到小数问题。对我来说,parseInt
解决方案似乎是最安全的。
ToString
相当直观。我想我本可以优化整个分组部分,但这样看起来也还可以。基本上,我们取一个数值,调用tostring
,分割小数,然后将整数格式化为组,并在添加小数后再次使用正确的小数分隔符。完成所有这些之后,如果数字是负数,我们会用负号修饰数字并返回生成的string
。
Using the Code
现在我们已经介绍了主要方法的构建方式,让我们看看如何智能地使用它。首先,这是最简单的使用方法
var formatter = new Formatting.NumberFormatter(new Formatting.NumberFormatInfo());
$(thisControlSelector).val(formatter.ToString(123456.789));
var value = formatter.Parse($(thisControlSelector).val());
这将创建一个基本的格式化器,使用默认的NumberFormatInfo
值(美国格式)。然后,数字将以控件的值“123,456.789
”写入,然后再次解析回变量“value
”。您将在示例项目中找到一种方法,可以使用服务器端的CultureInfo
数据,并通过数据合约将其发送回客户端,NumberFormatter
可以直接使用这些数据。现在,如果有人想使用专门的数字格式,例如来解析和编写会计报告中使用的数字,他们只需要定义自己的专门的NumberFormatInfo
类,然后该类可以用于服务器端并发送回客户端,以供NumberFormatter
使用。为了启用智能感知并验证所有必需的属性都存在,人们会创建一个客户端NumberFormatInfo
实例,将 Web 服务的返回值作为参数传递,如下面的示例代码所示。
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());
}
结论
我们已经看到了如何封装数字解析和写入可以简化程序员在编写客户端逻辑时的生活。在专业网站中,完全以异步方式编程页面并使其尽可能独立于服务器变得越来越流行。有了这类类,现在只需要访问 Web 服务(例如 REST 服务)来获取数据和保存数据,而无需依赖 Web 服务器进行数字格式化。显然,这个类本身是不够的,我肯定会添加其他的。下一个是日期,就像数字一样,我们有大量的日期选择器,但大多数没有专门的格式化器,使我们能够读取和写入本地化日期。