一、背景
货币转换插件github地址
这是一个由我编写的轻量级数字转货币字符串插件, 因为发现各个国家的货币相干常识很乏味, 并且以后市场上相干插件比拟老, 所以也想与你分享这些乏味常识。
本插件能够通过用户输出一个数字, 将其转化成货币的字符串格局, 比如说如数字12.345则转换成货币字符串:
当今世界上货币的标识形式形形色色十分乏味, 比方"印尼"的小数点是"逗号", 印尼的千分号是"句点", 新加坡的货币符号竟然是"SGD", 日元的标识符号也是"¥", 既然遇到这么多奇奇怪怪的写法那当然是要把它对立起来解决啦。
二、货币常识小课堂
- 小数点
咱们国家应用"."(句点)作为小数点, 然而比方 '印尼'、'德国'、'巴西' 等国家都是应用","(逗号)标识小数点:
中国: 123.45印尼: 123,45
- 千分位
与小数点一样, 咱们国家应用","(逗号)作为千分位, '印尼'等国家应用"."(句点)标识千分位:
中国: 12,345,678印尼: 12.345.678
- 准确位数
咱们国家比拟常见是保留两位小数, 也就是准确到"元角分"的"分", 然而比方"越南盾"就是没有小数的概念, 可能是因为"越南盾"动辄几万所以小数价格简直没有意义了:
2022/5/20: 1人民币 ≈ 3400 越南盾
为帮忙大家了解能够一起看看这个新闻:
- 货币符号的长短与地位
咱们熟知的是 "¥" "$" 他们都只是应用一个符号来示意, 其实世界上还有好多长度大于1的标识办法:
印尼: Rp 123新加坡: SGD 123蒙古语: CN¥ 123
岂但长度不同, 连货币符号的地位也不同, 不少国家是将货币符号放前面的:
中国 ¥ 123德国 123 越南 123 冰岛 123 ISK
- 货币符号的反复
比方日元应用的也是 "¥":
- 正数的示意办法
先看下excel外面的示意形式:
能够看出默认是"负号 + 货币符号 + 金额", 但我通过网络发现不少人网站采纳的是 "货币符号 + 负号 + 金额"的写法, 那么我的了解是须要给予用户自由选择的权力。
中国-¥123¥-123新加坡-SGD123SGD-123越南: 货币符号在前面不存在这些懊恼-123
三、网上已有的计划
第一个是: accounting.js仓库地址(4.8k Star) 应用的人多, 不太好的点是须要用户来指定展现规定, 也就是默认翻译成美元, 其余的翻译模式与货币符号都须要用户自定义:
并且大量的issue指出这个库的计算精度有bug。
11年前的老库, 并且没有ts反对。
第二个是: currencyFormatter.js仓库地址(632 Star) 其内所有的配置全靠代码全列举的写法不切实不优雅, 说白了就是每种语言如何展现都是内置在插件里的, 然而用户无奈应用自由组合的展现形式, 比方 $123.4万
这种组合展现模式:
6年前的老库, 须要实时更新配置文件, 并且没有ts反对。
四、制作插件前的需要剖析
学习完上述常识后, 咱们就能够在做"插件"之前进行一下需要的统计:
- 需要: 可将数字转换为任意国家货币格局。
- 需要: 可管制千分号的显隐。
- 需要: 可自在指定金额的计算形式"四舍五入", "向下取整", "向上取整"。
场景: 默认是四舍五入的计算形式, 然而数量级大了的状况下误差也会比拟大, 比方商家的 1件商品 1元钱, 达人带货能够取得 1.4%的佣金, 所以每件商品是 1 * 0.014 , 依照四舍五入计算会变成0.01元, 然而向上取整的话就是0.02元, 两种展现形式差异是很大的。 - 需要: 可自在指定保留小数的位数, 比方用户理论场景中须要让越南盾准确到一位小数。
场景: 还是卖货的例子, 比方商家的 1件商品 1元钱, 达人带货能够取得 1.4%的佣金, 所以每件商品是 1 * 0.014, 然而我国默认是保留两位小数, 此时能够让用户指定保留n位小数。 - 需要: 提供办法, 返回详尽的货币信息, 辅助用户玩出花色。
场景: format办法只返回格式化后的货币字符串, 并且要提供一个办法返回大而全的信息不便用户自在组装展现形式, 返回值的格局如下:
// 比方格式化 12345.67 为人民币, 返回的详细信息{ isFront: true, // 货币符号是否在金额后方 currencySymbol: "¥" // 货币符号 formatValue: "12,345.67", // 货币 value: 12345.67, // 本来的值 currencyString: "¥12,345.67", // 格式化后的货币 negativeNumber: false, // 是否为正数}
五、Intl.NumberFormat 何许人也
原生办法 Intl.NumberFormat 是对语言敏感的格式化数字类的结构器类
Intl.NumberFormat MDN
明天的配角此时才捷足先登, 咱们能够应用Intl.NumberFormat办法结构出, 浏览器原生反对的格式化货币的办法, 先看下根底用法:
var number = 123456.789;// 德语应用逗号作为小数点,应用.作为千位分隔符console.log(new Intl.NumberFormat('de-DE').format(number));// → 123.456,789// 大多数阿拉伯语国家应用阿拉伯语数字console.log(new Intl.NumberFormat('ar-EG').format(number));// → // India uses thousands/lakh/crore separatorsconsole.log(new Intl.NumberFormat('en-IN').format(number));// → 1,23,456.789
pc端的话ie浏览器对Intl.NumberFormat办法的兼容性不好, 它的兼容性如下所示:
六、如何指定 国家&货币符号, 我就乱指定了会怎么样?
比方咱们要转化成中国地区的中国货币格局:
new Intl.NumberFormat( 'zh', { style: 'currency', currency: 'CNY' }).format(12.345);// ¥12.35
下面代码中的 'zh' 参数代表中国地区, currency: 'CNY' 代表应用人民币符号, 我整顿了如何查问指定地区的code的网站:
查问国家代码: BCP 47 language tag
查问货币代码: ISO 4217 currency codes
乱指定货币会怎么样?
每次学习一个新的api总是忍不住试试不按标准填写会产生什么, 比方上面这样:
// 土耳其new Intl.NumberFormat( 'tr-TR', { style: 'currency', currency: 'TRY' }).format(12345.678);// 12.345,68// 我将地区指定为'土耳其' 货币指定为 '人民币'new Intl.NumberFormat( 'tr-TR', { style: 'currency', currency: 'CNY' }).format(12345.678);// CN¥12.345,68
下面能够看出货币的小数点与千分位的写法还是'土耳其'的写法标准, 然而货币符号变成了'CN¥', 也就是因为不止一个国家应用¥符号, 所以土耳其当地须要后面加上 CN来辨别国家, 所以说本国家展现本国家货币则间接应用本来的货币符号, 而不是中国国内应用CN¥, 而是间接¥。
被本地化的符号
人民币的 '¥' 在土耳其变成了 'CN¥', 我在网上搜寻了一下发现两种写法都标识人民币, 所以它是土耳其本地用来辨别货币符号的展现形式, 此时我忽然想到日元也是 ¥ , 那么土耳其是如何展现这些同样应用 ¥ 符号的?
new Intl.NumberFormat( 'tr-TR', { style: 'currency', currency: "JPY", }).format(12345.678);// ¥12.346 日元默认没有小数
日元是间接展现¥, 所以须要通过写法的不同来辨别国家。
七、面对多个国家
我理论业务中遇到了这个场景, 须要把数字转换成多个国家的货币, 所以咱们这个插件须要反对如下的应用形式。
初始化各种配置参数, 假如咱们的插件导出一个 CurrencyFormat 办法, addFormatType增加格式化配置的时候必须指定一个name, 不便后续调用指定的办法:
上面是我的插件的用法
const currencyFormat = new CurrencyFormat(); currencyFormat.addFormatType("人民币", { locale:'zh', currency: "CNY" // ... 其余配置 }); currencyFormat.addFormatType("新加坡", { locale:'zh-SG', currency: "SGD", // ... 其余配置 }); currencyFormat.addFormatType("日元", { locale:'ja-JP', currency: "JPY", // ... 其余配置 });
这里是应用的形式:
currencyFormat.format('人民币', 12.34)currencyFormat.format('新加坡', 12.34)currencyFormat.format('日元', 12.34)
上述的形式就能够实现, 只配置一次, 即可随处调用。
理论写一下根底代码构造:
class CurrencyFormat { formatObj = new Map(); addFormatType(typeName, options) { const { locale, currency } = options; const formatFn = new Intl.NumberFormat(locale, { currency, style: "currency" }).format; this.formatObj.set(typeName, { formatFn }); } format(typeName, val) { const formatItem = this.formatObj.get(typeName); if (formatItem) { const { formatFn } = formatItem; return formatFn(val); } return "-"; }}
实例化 Intl.NumberFormat, 而后每次调用对应的实力。
八、暗藏与展现千分号
这个能够很间接的通过一个属性即可, useGrouping 为 false的时候则为不展现'千分号'。
new Intl.NumberFormat(locale, { currency, style: "currency" useGrouping: false });
九、自在指定保留位数
比方中国默认保留两位小数, 然而用户须要查看小数点后三位的数字, 此时就有必要让用户指定货币格式化后要保留的位数, 参数名称为 maximumFractionDigits:
要留神, 如果 maximumFractionDigits 是个正数的话会报错:
new Intl.NumberFormat(locale, { currency, style: "currency" useGrouping: false, maximumFractionDigits: 3 });
这里重点是如果用户未指定 maximumFractionDigits, 那么此时就是用户应用的 '国家地区' 的保留小数默认值, 在我的插件里这个默认值后续会用来进行指定计算,那么我要如何晓得以后的计算是准确到几位小数?
很遗憾原生没有提供获取某个地区的计算精度的办法, 所以须要人为的计算出来:
计算的时候要留神非凡的打印格局:
// 通过编号零碎中的nu扩大键申请, 例如中文十进制数字console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));// → 一二三,四五六.七八九
形式一:
正则匹配, 如果数字1.234567转换后的'货币字符串'比方 '$1.23' 或者'1.23' , 进行从后往前的匹配, 从遇到到第一个数字开始记录, 如果遇到 '.' 或 ',' 则进行匹配返回构造的长度。
但其实有更优雅的正则形式, 当我传入0进行格式化时, 只须要匹配出 "数字1 + 小数点 + 数字2" 的模式中的数字2的长度即可:
let maxFractionDigits = 0; const currencyTempString = formatFn(0).replace(/\s/g, ""); const regVal = /[0〇]+[\.\,]([0〇]+)/g; const resArr = regVal.exec(currencyTempString); if (resArr) { maxFractionDigits = resArr?.[1]?.length; }
形式二:
第一个数字输出数字0, 转换后的'货币字符串'比方 '$0.00' 或者'0.00', 第二个数字输出0但同时指定保留小数0位, 而后将两个字符串长度相减后再减一, 限度这个数最小为0。
伪代码: 默认准确位数 = Math.max( '$0.00'长度 - '$0'长度 - '.'长度 , 0)。
这个形式有个大坑, 就是node 13版本之前执行可能会报错, 因为此时 maximumFractionDigits 传入0的时候可能会报错!
十、可抉择的"四舍五入"、"向下取整"、"向上取整"
Intl.NumberFormat 办法默认是 '四舍五入'的形式来计算金额, 但理论场景很可能须要用户来指定"向下取整"与"向上取整"这样的规定, 我这边定义为通过 calculationType: 'ceil' | 'floor' 来管制, 同时须要maxFractionDigits 这个参数来指定须要保留几位小数:
插件内的应用办法:
currencyFormat.addFormatType("人民币", { locale:'zh', currency: "CNY" calculationType: 'ceil' });
插件的format代码:
format(typeName, val) { const formatItem = this.formatObj.get(typeName); if (formatItem) { const { formatFn, calculationType, maxFractionDigits } = formatItem; const multiple = Math.pow(10, maxFractionDigits); if (calculationType === "ceil" || calculationType === "floor") { val = Math[calculationType](val * multiple) / multiple; } const currencyString = formatFn(val); return currencyString; } return "-"; }
这个代码里咱们获取到 calculationType 变量指定的计算类型, 而后将用户传入的数据先乘上 10的maxFractionDigits 次方, 而后进行Math运算, 计算好后再除以 10的 maxFractionDigits 次方。
十一、对正数的兼容
如果货币为正数, 比方'-12.34'则插件默认返回"-$12.34", 然而有些场景须要展现为"$-12.34", 如果地区指定为新加坡则展现"-SGD12.35", 显著负号在外层有点看不清了, 那么接下来就解决这个问题。
十二、返回详细信息
插件默认返回的格局是"$12.34", 然而实际上可能咱们要批改一下款式, 比方在货币符号与金额中减少空格 "$ 12.34", 或者须要暗藏符号只展现"12.34", 还有就是上述的'正数'问题, 所以有必要返回详情给用户:
// 比方格式化 12345.67 为人民币{ isFront: true, // 货币符号是否在金额后方 currencySymbol: "¥" // 货币符号 formatValue: "12,345.67", // 货币 value: 12345.67, // 本来的值 currencyString: "¥12,345.67", // 格式化后的货币 negativeNumber: false, // 是否为正数}
新增formatDetail办法, 这里先将数字取绝对值后再进行货币格式化, 代码为:
getFrontCurrencySymbol = (val) => /^[^\d一二三四五六七八九]+/g.exec(val)?.[0] ?? ""; getAfterCurrencySymbol = (val) => /[^\d一二三四五六七八九]+$/g.exec(val)?.[0] ?? ""; formatDetail(typeName, val) { const value = Math.abs(val); const currencyString = this.format(typeName, value); const frontCurrencySymbol = this.getFrontCurrencySymbol(currencyString); if (frontCurrencySymbol) { return { isFront: true, currencySymbol: frontCurrencySymbol, formatValue: currencyString.slice(frontCurrencySymbol.length) || "0", value, currencyString, negativeNumber: val < 0 }; } const afterCurrencySymbol = this.getAfterCurrencySymbol(currencyString); return { isFront: false, currencySymbol: afterCurrencySymbol, formatValue: currencyString.slice(0, -afterCurrencySymbol.length) || "0", value, currencyString, negativeNumber: val < 0 };
如下的应用形式与返回值:
currencyFormat.addFormatType("中文汉字", { locale:'zh-Hans-CN-u-nu-hanidec', currency: "CNY", }); console.log('中文汉字',currencyFormat.formatDetail('中文汉字', 12345.67));
十三、缩写
所谓缩写就是图里这种模式:
也就是如何展现长数字, 我这里减少了一个formatAbbreviation办法, 此办法能够返回数字被缩写解决后的字符串, 用法如下 :
const currencyFormat = new CurrencyFormat();currencyFormat.addFormatType("en_gb", { locale: "en-GB", currency: "GBP",});currencyFormat.formatAbbreviation("en_gb", 123456.789)// 打印后果是 £123K
配置计划如下, 比方想让越南盾按中文进行缩略:
const currencyFormat = new CurrencyFormat(); currencyFormat.addFormatType("demo_越南_中文", { locale: 'vi-VN', currency: "VND", validAbbreviations: { "3": '千', "4": '万', "8": "亿", "13": "兆" }, });
validAbbreviations 属性指定了当数字超过多少位时进行缩略, 并且指明缩略后的符号, 比方上图就是 '1000' 转换为 '1千'。
当然咱们默认内置了一套根底的转换规则:
validAbbreviationsTypeEN = { 3: "K", 6: "M", 9: "B", 12: "T", };
那么它的原理我简略论述下, 其实我是利用了已有的api "formatDetail"来获取到货币详情对象, 此时拿到 实在的数值, 对这个数值与 validAbbreviationsTypeEN进行循环比拟 , 看他的位数应该加什么缩略符号。
然而也要留神一点, 就是缩略符号的地位, 因为货币符号有前有后, 所以当货币符号在前则没有非凡解决, 然而货币符号在后, 就须要先展现缩略符再展现货币符, 例如下方展现的:
123456.789 --> Rp12万 123456.789 --> 12万
十四、可指定符号
实在业务给我上了一课, 不是所有场景都按一些国际化规范进行的, 比方针对新加坡联盟这边须要展现 "S$", 然而其国内上应该展现"$"即可, 此时不想按规范展现符号就须要咱们能够让开发者自在指定货币符号, 所以我新增了 targetCurrency 属性:
const currencyFormat = new CurrencyFormat();currencyFormat.addFormatType("en_gb_targetCurrency1", { locale: "en-GB", currency: "GBP", targetCurrency: "xxxx"});currencyFormat.format("en_gb_targetCurrency1", 123456.789)// 最初输入的后果是"xxxx123,456.79
原理也比拟间接, 在format办法的最终, 判断是否有targetCurrency属性, 而后将其与以后的货币符号进行替换即可。
end
这次就是这样, 心愿与你一起提高。