学习PHP中国际化地数字格式处理
不知道大家有没有了解过,对于数字格式来说,西方国家会以三位为一个进位,使用逗号来分隔。比如,12345678,用标准的格式来表示的话就是 12,345,678 。不过我们中文其实并不会有这样的分隔符,另外像某些地区则是以空格为分隔的,这个我们马上通过代码就可以看到。其实在之前的文章中我们就已经接触过一点这方面的知识,学习PHP中的国际化功能来查看货币及日期信息,今天就来详细的学习一遍。至于为什么要格式化数字、货币这些内容呢?我们将在文章讲解中逐一说明。
数字标准格式
首先还是看我们开头介绍的标准数字格式。
$localeArr = ['en_US', 'zh_CN', 'ja_JP', 'de_DE', 'fr_FR', 'ar-IQ', 'ru_RU'];
foreach ($localeArr as $locale) {
$fmt = new NumberFormatter($locale, NumberFormatter::DECIMAL);
echo $locale . ':', $fmt->format(1234567.891234567890000), PHP_EOL;
}
// en_US:1,234,567.891
// zh_CN:1,234,567.891
// ja_JP:1,234,567.891
// de_DE:1.234.567,891
// fr_FR:1 234 567,891
// ar-IQ:١٬٢٣٤٬٥٦٧٫٨٩١
// ru_RU:1 234 567,891
我们先指定了许多的国家地区编码,然后循环它们,使用 NumberFormatter 对象来对他们进行实例化。第二个参数就是要实例化的格式类型,这里我们指定的是数字类型。然后使用 format() 方法就可以对指定的数字进行格式化地输出了。可以看到,德国是使用 . 来分隔进位,使用逗号来做为小数点。而法国和俄罗斯则是使用空格来表示进位,逗号表示小数点。其它国家则是沿用标准的英式表示。
对于很多财务及银行项目来说,标准数字格式非常有用。往往我们接触到比较多的是在汇款时要填写的普通数字、中文大写,而一些面向企业和涉外的公司财务也需要这种标准格式的数字来进行存根的记录。既然说到财务了,我们再看看货币格式的展示。
货币格式
foreach ($localeArr as $locale) {
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
echo $locale . ':', $fmt->format(1234567.891234567890000), PHP_EOL;
echo $locale . ':', $fmt->formatCurrency(1234567.891234567890000, 'RUR'), PHP_EOL;
}
// en_US:$1,234,567.89
// en_US:RUR 1,234,567.89
// zh_CN:¥1,234,567.89
// zh_CN:RUR 1,234,567.89
// ja_JP:¥1,234,568
// ja_JP:RUR 1,234,567.89
// de_DE:1.234.567,89 €
// de_DE:1.234.567,89 RUR
// fr_FR:1 234 567,89 €
// fr_FR:1 234 567,89 RUR
// ar-IQ:١٬٢٣٤٬٥٦٨ د.ع.
// ar-IQ:١٬٢٣٤٬٥٦٧٫٨٩ RUR
// ru_RU:1 234 567,89 ₽
// ru_RU:1 234 567,89 р.
在这段代码中,我们使用了两种模式的输出。第一个是指定 NumberFormatter 的第二个参数为 CURRENCY ,也就是指定格式化为货币格式。其实就是为标准格式的数字前后增加了对应地区的代币符号。比如我们中国和日本通用的 ¥ ,一般是放在金额的前面,而欧洲的则使用 € 欧元标识放在金额的后面。
另一种形式就是 formatCurrency() 这个方法可以指定一个货币类型,如果不是这个类型的区域设置的话,就直接输出这个货币字符。在测试代码中,我们给定的是俄罗斯的老卢布,其它区域中会直接输出 RUR ,而在区域设置为俄罗斯时,输出的就是标准的老卢布符号(现在使用的是新卢布,符号是 ₽ ,老卢布就是 р.)。
详细的地区格式化样式
是不是感觉已经很高大上了?不不不,上面两种格式只是开胃菜,真正好玩的现在马上端给你。
$fmt = new NumberFormatter('zh_CN', NumberFormatter::PERCENT);
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 123 456 789 %
$fmt = new NumberFormatter('zh_CN', NumberFormatter::SCIENTIFIC);
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 1,2345678912345679E6
$fmt = new NumberFormatter('zh_CN', NumberFormatter::SPELLOUT);
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 一百二十三万四千五百六十七点八九一二三四五六七九
$fmt = new NumberFormatter('zh_CN', NumberFormatter::SPELLOUT);
echo $fmt->format(1234502.891234567890000), PHP_EOL; // 一百二十三万四千五百〇二点八九一二三四五六七九
$fmt = new NumberFormatter('zh_CN', NumberFormatter::ORDINAL);
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 第1,234,568
$fmt = new NumberFormatter('zh_CN', NumberFormatter::DURATION);
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 1,234,568
PERCENT 不多说了,百分比,就是增加了一个百分号,而且不是以标准格式输出的,会以空格进行进位分隔。SCIENTIFIC 就是我们常见的科学计数法,测试代码中的结果就是 1.xx 的 10 的 6 次方的意思。
SPELLOUT 就比较厉害了,按当前区域语言的拼写规则。没错,直接转换成了我们的中文表示。如果需要再转换成中文的大写,直接字符替换就可以了,这个绝对是这次文章的重大发现。之前在一家公司面试的时候就有人问过如何将数字转换成中文表示,因为很多的财务系统都需要这样的功能。不管是做帐还是处理发票,中文大写或小写都是系统自动输出的。当时还写了半天算法,如果大家自己写算法的时候除了需要注意单位外,零的表示也是非常重要的一点,有兴趣的朋友可以自己尝试一下。不过下回如果面试的时候有人问这个问题,那我直接就会甩出 NumberFormatter::SPELLOUT 这个神器了。
ORDINAL 是排序的表示,在中文中其实就是在前面增加了一个 第 字。DURATION 是基于持续时间规则的格式。这两种都会抛弃掉小数点。
格式化规则设置
虽说已经有这么多的规则格式供我们使用了,但大家的业务总是千奇百怪的,我们能不能定义自己的格式规则呢?既然这么写了,那当然是可以的啦。
var_dump($fmt->getPattern()); // string(8) "#,##0.##"
$fmt->setPattern("#0.# kg");
var_dump($fmt->getPattern()); // string(6) "0.# kg"
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 1234567.9 kg
看出来了吗?我们使用 setPattern() 方法来定义了一个带 kg 的格式规则,很显示,我们是需要一个表示重量的格式。然后仅保留一位小数点,不需要分隔符号。这样再次使用 format() 方法的时候就会按照我们指定的格式来进行格式化了。
属性操作
当然,除了直接设置规则格式外,我们还可以指定一些属性值来改变当前的格式效果。
$fmt = new NumberFormatter( 'zh_CN', NumberFormatter::DECIMAL );
echo "Digits: ".$fmt->getAttribute(NumberFormatter::MAX_FRACTION_DIGITS), PHP_EOL; // Digits: 3
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 1,234,567.891
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
echo "Digits: ".$fmt->getAttribute(NumberFormatter::MAX_FRACTION_DIGITS), PHP_EOL; // Digits: 2
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 1,234,567.89
这段代码中,我们通过 setAttribute() 来设置 MAX_FRACTION_DIGITS 的值,用于改变最大保留的小数点位数。当然,不仅限于这一个属性,还有很多别的可以修改的属性,大家可以自行查阅官方手册。
分隔符号设置
同样,我们可以直接修改格式化中的分隔符、小数点等使用的符号。直接使用 setSymbol() 方法就可以。
var_dump($fmt->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL)); // string(1) ","
$fmt->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, "*");
var_dump($fmt->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL)); // string(1) "*"
echo $fmt->format(1234567.891234567890000), PHP_EOL; // 1*234*567.891
与地区格式化关联的文本属性设置
我们还可以直接设置与地区格式化相关的一些文本信息,比如下面代码中使用 setTextAttribute() 修改了负号的表示。我们还可以使用这个方法修改间隔字符,货币编码等内容,大家可以自己对照官方文档测试学习。
var_dump($fmt->getTextAttribute(NumberFormatter::NEGATIVE_PREFIX)); // string(1) "-"
echo $fmt->format(-1234567.891234567890000), PHP_EOL;
$fmt->setTextAttribute(NumberFormatter::NEGATIVE_PREFIX, "负号 ");
var_dump($fmt->getTextAttribute(NumberFormatter::NEGATIVE_PREFIX)); // string(7) "负号 "
echo $fmt->format(-1234567.891234567890000), PHP_EOL; // 负号 1,234,567.891
获取地区信息
这两个方法就是简单地获取当前的地区信息了,之前在其它的文章中我们也讲过,VALID_LOCALE 是表示有效区域,ACTUAL_LOCALE 表示的是实际区域。
var_dump($fmt->getLocale(Locale::VALID_LOCALE)); // string(10) "zh_Hans_CN"
var_dump($fmt->getLocale(Locale::ACTUAL_LOCALE)); // string(10) "zh_Hans_CN"
字符转换为数字、货币格式
我们能够将数字进行格式化地输出,输出之后的内容因为增加了分隔符之类的内容,所以都会转成字符串,那么,我们能不能把已经格式化过的标准数字字符再转回数字类型呢?
$fmt = new NumberFormatter( 'zh_CN', NumberFormatter::DECIMAL );
$num = "1,234,567.891";
echo $fmt->parse($num)."\n"; // 1234567.891
echo $fmt->parse($num, NumberFormatter::TYPE_INT32)."\n"; // 1234567
$fmt = new NumberFormatter( 'zh_CN', NumberFormatter::CURRENCY );
echo $fmt->parseCurrency('¥1,234,567.89', $currency), PHP_EOL; // 1234567.89
var_dump($currency); // string(3) "CNY"
两个方法,第一个是 parse() 方法,将标准格式的数字字符串转回指定类型的数字,可以指定为 TYPE_INT32 、TYPE_INT64 、TYPE_DOUBLE 、TYPE_CURRENCY 等类型。另外一个方法是 parseCurrency() 方法,从名字就可以看出,它是将货币格式转回数字,并且,很重要的一点是,它的第二个引用参数,可以将货币符号的通用编码也返回回来,比如测试代码中返回的 CNY 代表的就是我们使用的人民币。
错误信息
最后我们来看看 NumberFormatter 中的错误信息如何获取。
echo $fmt->parseCurrency('1,234,567.89', $currency), PHP_EOL;
var_dump($fmt->getErrorCode()); // int(9)
var_dump(intl_is_failure($fmt->getErrorCode())); // bool(true)
var_dump($fmt->getErrorMessage()); // string(36) "Number parsing failed: U_PARSE_ERROR"
在这里我们使用非标准的货币字符串来使用 parseCurrency() 进行转换,parseCurrency() 必须接收的是带货币符号的内容,所以这里就产生了错误。我们使用 getErrorCode() 可以获取到错误码,使用 getErrorMessage() 可以获取到错误信息。另外是一个 intl_is_failure() 函数,用于根据错误码判断是否产生了区域语言问题的错误。
总结
又是大开眼界的一次学习旅程,中文小写格式的转换真的是之前完全不知道的,而货币的互相转换我觉得也完全可以应用到一些采集程序中,比如电商页面价格的采集分析。总之,还是感觉到收获满满的。另外,这一套 NumberFormatter 对象也是提供了面向过程的函数式使用方法的,比如 numfmt_create() ,记住是 numfmt_ 开头的函数哦,不要和 number_format() 相关的函数搞混了。
测试代码:
https://github.com/zhangyue0503/dev-blog/blob/master/php/202011/source/4.学习PHP中国际化地数字格式处理.php
参考文档:
https://www.php.net/manual/zh/class.numberformatter.php
关注公众号:【硬核项目经理】获取最新文章
添加微信/QQ好友:【xiaoyuezigonggong/149844827】免费得PHP、项目管理学习资料
知乎、公众号、抖音、头条搜索【硬核项目经理】
B站ID:482780532