使用 Dinero.js 格式化非十进制货币:单细分与多细分货币的完整实践指南

📅 发布时间:2026/10/9 1:41:18
使用 Dinero.js 格式化非十进制货币:单细分与多细分货币的完整实践指南
金融科技【免费下载链接】dinero.jsCreate, calculate, and format money in JavaScript and TypeScript项目地址https://gitcode.com/gh_mirrors/di/dinero.js点击查看免费下载绝大多数流通货币都是十进制的但非十进制货币在真实世界中依然存在——古代希腊德拉克马drachma1 德拉克马 6 奥波勒斯 obol、1971 年之前的英镑1 镑 20 先令1 先令 12 便士、毛里塔尼亚乌吉亚ouguiya基数为 5等。此外虚构货币如《哈利·波特》中的巫师货币1 加隆 17 西可1 西可 29 纳特也是典型的非十进制场景。如果你在开发钱币学网站、历史主题应用或带自有货币系统的游戏就很可能遇到这类格式化需求。本文将基于 docs/guides/formatting-non-decimal-currencies.md 指南结合 Dinero.js 的toUnits源码与测试讲解如何使用 Dinero.js 处理单细分和多细分的非十进制货币并输出符合业务语境的格式化文本。读完本文你将能自定义DineroCurrency对象、利用toUnits的 transformer 机制完成任何非十进制货币的展示。非十进制货币在 Dinero.js 中如何表示要理解格式化先要理解 Dinero.js 如何用三个字段描述一种货币。根据 currency 核心概念一个DineroCurrency由三部分组成code货币的唯一标识符例如GRDbase基数radix即该货币最小细分单位之间的进制关系。十进制货币 base 为10非十进制货币可以是任意整数如6exponent指数表达货币与其最小细分单位的十进制关系。对于非十进制货币官方约定exponent 必须设为1。这从类型定义也能看到DineroCurrency 类型 中base的类型为TAmount | readonly TAmount[]既支持单个基数也支持用数组表达多级细分。同时根据 amount 核心概念amount 总是以最小细分单位的整数表达。例如 1 德拉克马 6 奥波勒斯那么dinero({ amount: 6, currency: GRD })就表示 1 德拉克马同理1971 年前的英镑 50 镑 1000 先令 12000 便士amount 应写成12000。import { dinero } from dinero.js; // Ancient Greek drachma1 德拉克马 6 奥波勒斯 const GRD { code: GRD, base: 6, exponent: 1, }; // 表示 1 德拉克马即 6 奥波勒斯 const d1 dinero({ amount: 6, currency: GRD }); // 十进制化之前的英镑1 镑 20 先令1 先令 12 便士 const GBP { code: GBP, base: [20, 12], exponent: 1, }; // 表示 50 镑即 1000 先令或 12000 便士 const d2 dinero({ amount: 12000, currency: GBP });格式化单一细分的非十进制货币对于只有一个细分单位的非十进制货币如德拉克马只有奥波勒斯一层细分开箱即可用toUnits完成格式化。toUnits会把一个 Dinero 对象的总金额拆分为各层单位 剩余最小单位的数组。例如$10.45amount 为1045会返回[10, 45]。对非十进制货币同样适用import { dinero, toUnits } from dinero.js; const GRD { code: GRD, base: 6, exponent: 1 }; const d dinero({ amount: 9, currency: GRD }); toUnits(d); // [1, 3]即 1 德拉克马 3 奥波勒斯不过[1, 3]这样的裸数组通常不满足业务展示需求。此时可以传入可选的 transformer 函数把拆分结果转换为可读字符串。transformer 接收{ value, currency }两个字段返回任意类型DineroTransformer 类型定义。原指南给出了一个结合pluralize库的完整示例import { dinero, toUnits } from dinero.js; import pluralize from pluralize; const labels [drachma, obol]; function transformer({ value, currency }) { return value .filter((amount) amount 0) .map((amount, index) ${amount} ${pluralize(labels[index], amount)}) .join(, ); } const d dinero({ amount: 9, currency: { code: GRD, base: 6, exponent: 1, }, }); toUnits(d, transformer); // 1 drachma, 3 obols这里有几个值得注意的实践要点.filter((amount) amount 0)跳过值为 0 的单位层避免出现 0 obols 这类冗余文本。测试用例也验证了中间层出现零值的情况见下文pluralize(labels[index], amount)按数量自动处理单复数transformer 的返回值不限于字符串可以是任意TOutput类型完全由你的业务决定。格式化多细分的非十进制货币大多数流通货币只有一层细分单位但许多古代货币拥有多级细分。法国旧制度下的里弗尔图尔努瓦livre tournois、1971 年前的大不列颠英镑都如此一些虚构货币也不例外。对于多级细分货币你可以在base字段传入数组按从高到低的顺序声明每一级细分之间的进制关系。原指南用甜甜圈游戏内货币举例1 个甜甜圈donut 30 块曲奇cookie1 块曲奇 16 根棒棒糖lollipop一个价值 720 根棒棒糖的奖励应格式化为 1 donut and 15 cookies。import { dinero, toUnits } from dinero.js; const POP { code: POP, base: [30, 16], // 1 donut 30 cookies, 1 cookie 16 lollipops exponent: 1, }; const labels [donut, cookie, lollipop]; function transformer({ value }) { return value .filter((amount) amount 0) .map((amount, index) ${amount} ${amount 1 ? ${labels[index]}s : labels[index]}) .join( and ); } const d dinero({ amount: 720, currency: POP }); toUnits(d, transformer); // 1 donut and 15 cookies注意拆解逻辑720 根棒棒糖 720 / 16 45 块曲奇余 045 块曲奇 45 / 30 1 个甜甜圈余 15所以结果是 1 个甜甜圈和 15 块曲奇toUnits返回的原始数组为[1, 15, 0]0 根棒棒糖被 filter 过滤掉了。多级细分的经典现实例子在 toUnits API 文档 和测试用例中都有出现——1971 年前的英镑import { dinero, toUnits } from dinero.js; const GBP { code: GBP, base: [20, 12], exponent: 1 }; const d dinero({ amount: 267, currency: GBP }); toUnits(d); // [1, 2, 3]即 1 镑 2 先令 3 便士 const labels [pounds, shillings, pence]; toUnits(d, ({ value }) value .filter((amount) amount 0) .map((amount, index) ${amount} ${labels[index]}) .join(, ) ); // 1 pounds, 2 shillings, 3 pencetoUnits 底层原理除数计算与逐层拆解toUnits之所以能同时处理十进制、单一非十进制和多级细分货币关键在于它对base数组的统一处理。查看核心实现 core/api/toUnits.tsexport function toUnitsFnTCurrency extends string( ...[dineroObject, transformer]: ToUnitsParamsTAmount, TOutput, TCurrency ) { const { amount, currency, scale } dineroObject.toJSON(); const { power, integerDivide, modulo } calculator; const bases isArray(currency.base) ? currency.base : [currency.base]; const divisors getDivisorsFn(bases.map((base) power(base, scale))); const value divisors.reducereadonly TAmount[]( (amounts, divisor, index) { const amountLeft amounts[index]; const quotient integerDivide(amountLeft, divisor); const remainder modulo(amountLeft, divisor); return [...amounts.filter((_, i) i ! index), quotient, remainder]; }, [amount] ); if (!transformer) { return value; } return transformer({ value, currency }); }核心流程分三步归一化 baseisArray(currency.base) ? currency.base : [currency.base]把单个基数统一成数组这样单细分与多细分共用同一套拆解逻辑逐层计算除数调用getDivisors从最高层开始计算每一级单位对应的最小单位数。对[20, 12]而言除数序列为[20*12, 12]即[240, 12]——意味着 1 镑 240 便士、1 先令 12 便士迭代求商取余从amount开始依次用每个除数做整数除法得到该层数量余数继续传入下一轮。这就是先分成镑再把剩余的便士分成先令与便士的数学表达。getDivisors的实现也值得一看它用bases.slice(i).reduce((acc, curr) multiply(acc, curr))自右向左累积乘出每个层级的总进制天然支持任意层级的细分。在公共 API 层api/toUnits.tstoUnits只是从 dinero 对象取出calculator再委托给核心实现并提供了带 transformer / 不带 transformer两个 TypeScript 重载签名返回类型分别为readonly TAmount[]与TOutput。边界行为与测试佐证toUnits 测试用例 覆盖了 number、bigint、Big.js 三种实现其中非十进制相关用例直接印证了本文的所有示例场景输入输出单一细分GRD, base 6dinero({ amount: 9, currency: GRD })[1, 3]多级细分GBP, base [20, 12]dinero({ amount: 267, currency: GBP })[1, 2, 3]中间层零值GBP, base [20, 12]dinero({ amount: 2, currency: GBP })[0, 0, 2]中间层零值是最容易踩坑的边界toUnits会忠实返回每一位的数值包括 0。测试明确断言amount: 2的 GBP 返回[0, 0, 2]。因此 transformer 中必须自行决定是否过滤零值——这也是指南示例中普遍.filter((amount) amount 0)的原因。注意不要把[0, 0, 2]误判为金额有误它在语义上完全正确只是展示时通常需要精简。完整可运行的实战模板把上述要点整合一个可复制的完整示例兼容 number 与 bigint 两种 amount 类型import { dinero, toUnits } from dinero.js; // 游戏内货币1 donut 30 cookies1 cookie 16 lollipops const POP { code: POP, base: [30, 16], exponent: 1, }; const labels [donut, cookie, lollipop]; function formatMoney({ value }) { return value .filter((amount) amount 0) .map((amount, index) ${amount} ${amount 1 ? ${labels[index]}s : labels[index]}) .join( and ); } const bonus dinero({ amount: 720, currency: POP }); toUnits(bonus, formatMoney); // 1 donut and 15 cookies如果你的金额可能超出Number的安全整数范围请改用 bigint 变体货币与金额都换成 bigint详见 精度与大数指南import { dinero, toUnits } from dinero.js/bigint; const POP { code: POP, base: [30n, 16n], exponent: 1n }; const d dinero({ amount: 720n, currency: POP }); toUnits(d); // [1n, 15n, 0n]测试用例中 bigint 变体的断言如expect(toUnits(d)).toEqual([1n, 3n])与此完全一致说明toUnits对 bigint 金额的类型支持是完整的。常见问题与注意事项1. 非十进制货币的 exponent 为什么必须是 1根据 currency 文档 的约定非十进制货币应设 exponent 为1此时 scale 默认跟随 exponent。若你手动传入自定义 scale会覆盖 exponent 并改变金额的解读方式参考 scale 核心概念请谨慎使用。2. amount 必须以最小单位整数传入。Dinero.js 会在传入非整数时抛错见 amount 核心概念。表示 1 德拉克马要写amount: 6而不是amount: 1。3. 单位标签与金额数量分离。transformer 中labels数组的顺序必须与base数组以及toUnits返回数组的顺序严格对应索引 0 是最高单位索引递增对应逐级细分。4. 零值过滤与连接词。多单位展示时用filter去掉零值单位用join( and )/join(, )等连接词组装避免输出 0 lollipops。5. 自定义货币的 TypeScript 类型。若使用 TypeScript可声明DineroCurrencyTAmount类型约束你的自定义货币对象DineroCurrency 类型并配合as const satisfies启用编译期货币类型安全见 currency type safety 指南让 TypeScript 在编译期拦截用 USD 和 GRD 相加这类错误。6. 存储与恢复。非十进制货币通常不在dinero.js/currencies内置的 ISO 4217 列表中需自行持久化整个DineroCurrency对象。当金额使用自定义 scale 时数据库存储指南 提示不仅要存货币 exponent还要一并存储 scale才能准确恢复原对象。7. 内置货币的变动风险。内置 ISO 4217 货币会随标准修订更新跨版本可能变更。自定义的非十进制货币不受影响因为它们是你在应用内自行声明的对象详见 currency 文档 的警告说明。小结非十进制货币的格式化在通用支付类库中常被忽略但 Dinero.js 通过 base 数组 统一除数拆解 可插拔 transformer 的设计优雅地覆盖了它单细分货币如德拉克马用base: 6toUnits transformer 即可输出 1 drachma, 3 obols多级细分货币如十进制化前英镑、游戏内货币用base: [20, 12]或base: [30, 16]声明进制关系toUnits自动完成逐层拆解底层实现core/api/toUnits.ts 与 getDivisors.ts对所有 base 形态一视同仁行为由 toUnits 测试 全面锁定。无论是钱币学网站、历史题材应用还是带自有货币体系的游戏这套方案都能在保证整数精度无浮点误差的前提下输出符合业务语境的格式化文本。赞分享金融科技【免费下载链接】dinero.jsCreate, calculate, and format money in JavaScript and TypeScript项目地址https://gitcode.com/gh_mirrors/di/dinero.js点击查看免费下载相关推荐使用Dinero.js进行货币计算及格式化使用Dinero.js进行货币计算及格式化 一、项目介绍 Dinero.js是一款专为JavaScript和TypeScript设计的库允许您安全地创建、计算金融科技Dinero.js国际化指南轻松处理非十进制货币和特殊货币符号的终极教程Dinero.js国际化指南轻松处理非十进制货币和特殊货币符号的终极教程 在全球化应用开发中正确处理不同国家和地区的货币格式是提升用户体验的关键环节。Din金融科技Dinero.js终极指南如何正确处理日元、里亚尔等非十进制货币格式化Dinero.js终极指南如何正确处理日元、里亚尔等非十进制货币格式化 在现代全球化应用中正确处理各种货币格式是开发者的必备技能。Dinero.js作为一个金融科技上一篇JSON-java安全最佳实践防止JSON注入攻击的完整方案下一篇vxrn中的内容管理创建动态React Native内容应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考