金融科技【免费下载链接】dinero.jsCreate, calculate, and format money in JavaScript and TypeScript项目地址https://gitcode.com/gh_mirrors/di/dinero.js点击查看免费下载导读本文聚焦 Dinero.js 中用于操作变更货币金额的一组核心 API——add、subtract、multiply与allocate它们共同构成了mutation变更这一核心概念。文章将带你完整复刻一个典型结算页购物车小计、折扣分摊、运费合计的金额计算流程并深入讲解Dinero 对象不可变这一关键设计同时结合本仓库源码packages/dinero.js/src/core/api揭示每一步运算背后的同币种校验、scale 归一化与最安全精度转换机制。读完本文你将能够在真实项目中安全地组合这些函数理解它们何时返回新对象、如何处理不同精度、如何分配余数。Mutations操纵金钱的核心 API 集合在 Dinero.js 中操纵金额的核心手段就是 mutation 函数。它们大多基于算术运算加法、乘法、减法以及更复杂的按比例分配。与直觉相反这些函数虽然被归类为mutations变更却不会修改传入的对象——它们总是返回一个全新的 Dinero 对象。一个最基础的例子对应 docs/core-concepts/mutations.md 开头import { dinero, add } from dinero.js; import { USD } from dinero.js/currencies; const d1 dinero({ amount: 500, currency: USD }); const d2 dinero({ amount: 800, currency: USD }); add(d1, d2); // 返回 amount 为 1300 的新 Dinero 对象除了add完整的 mutation API 还包括subtract、multiply、allocate。你可以在 docs/api/mutations/add.md、docs/api/mutations/subtract.md、docs/api/mutations/multiply.md 与 docs/api/mutations/allocate.md 中查看每个函数的完整参数表与独立示例。计算新金额一个完整的结算页案例任何处理金钱的应用都必然需要操纵金额。最经典的场景是结算页你需要计算商品小计、加上运费、减去折扣等。原文档给出了一个非常完整的例子这里完整复刻并补充说明每一步的含义import { dinero, add, allocate, subtract } from dinero.js; import { USD } from dinero.js/currencies; const products [ { name: Apple iPhone 12, price: dinero({ amount: 89900, currency: USD }), }, { name: Apple AirPods Pro, price: dinero({ amount: 17495, currency: USD }), }, ]; // 用 reduce 把购物车中所有商品价格累加得到小计 const subtotal products.reduce( (acc, { price }) add(acc, price), dinero({ amount: 0, currency: USD }) ); // 按 20% / 80% 的比例分配小计取第一份作为折扣 const [discount] allocate(subtotal, [20, 80]); const discounted subtract(subtotal, discount); const shipping dinero({ amount: 1000, currency: USD }); const total add(discounted, shipping);这个例子串联了 mutation API 的四种典型用法add以dinero({ amount: 0, currency: USD })为零元累加器逐步累加商品价格allocate把subtotal按[20, 80]比例拆成两份这里只取第一份作为折扣金额subtract从小计中扣掉折扣add再加上运费得到最终total。注意金额的单位是最小货币单位cents例如 89900 表示 $899.00。关于 amount 与 scale 的详细约定可参阅 docs/core-concepts/amount.md 与 docs/core-concepts/scale.md。Dinero 对象是不可变的虽然这类函数被归类为 mutations但 Dinero 对象是不可变的immutable。使用任何 mutation 函数时传入的既有对象始终保持原样函数返回的是全新对象。原文档用toSnapshot直观地证明了这一点import { dinero, add, toSnapshot } from dinero.js; // 假设 d1 dinero({ amount: 500, currency: USD }) // 假设 d2 dinero({ amount: 800, currency: USD }) toSnapshot(add(d1, d2)); // { // amount: 1300, // currency: { // code: USD, // base: 10, // exponent: 2, // }, // scale: 2, // } toSnapshot(d1); // { // amount: 500, // currency: { // code: USD, // base: 10, // exponent: 2, // }, // scale: 2, // } toSnapshot(d2); // { // amount: 800, // currency: { // code: USD, // base: 10, // exponent: 2, // }, // scale: 2, // }执行add(d1, d2)之后d1仍是 500、d2仍是 800只有返回的新对象携带相加后的 1300。这一不变性对构建可预测的状态管理如 React 中的 reducer、函数式流水线至关重要——你可以在任何时刻放心保留对旧对象的引用而不用担心被改掉。从源码层面看这种不变性体现在每个 mutation 函数都通过create返回新对象。例如 core/api/add.ts 中const { amount: augendAmount, currency, scale } augend.toJSON(); const { amount: addendAmount } addend.toJSON(); const amount calculator.add(augendAmount, addendAmount); return augend.create({ amount, currency, scale, });它读取原对象的快照数据用 calculator 算出新金额再通过augend.create(...)构造并返回新Dinero 对象全程没有对原对象做任何写入。深入add与subtract同币种校验与 scale 归一化只允许相同币种相加/相减add与subtract都要求参与运算的对象必须使用同一种货币否则会抛出错误。其实现逻辑在 core/api/add.ts 与 core/api/subtract.ts 中如出一辙const condition haveSameCurrency([augend, addend]); assert(condition, UNEQUAL_CURRENCIES_MESSAGE); const [newAugend, newAddend] normalizeFn([augend, addend]); return addFn(newAugend, newAddend);即先通过haveSameCurrency校验币种不满足则抛出断言错误。错误消息定义在 core/checks/messages.tsexport const UNEQUAL_CURRENCIES_MESSAGE Objects must have the same currency.;在 TypeScript 中若配合类型化货币typed currencies使用这一约束还会在编译期被强制检查详见 guides/currency-type-safety.md。自动归一化到最高 scale币种相同还不够两个对象的scale小数位数可能不同。add/subtract会自动把两个对象归一化到最高的 scale后再运算。normalizeScale的实现位于 core/api/normalizeScale.ts它先求出所有对象 scale 的最大值然后对 scale 较低的对象调用transformScale提升精度。因此以下两个不同 scale 的对象相加结果是 scale 为 4 的新对象对应 docs/api/mutations/add.md 的示例import { dinero, add } from dinero.js; import { USD } from dinero.js/currencies; const d1 dinero({ amount: 400, currency: USD }); const d2 dinero({ amount: 104545, currency: USD, scale: 4 }); add(d1, d2); // amount 144545scale 4减法同理对应 docs/api/mutations/subtract.md 的示例import { dinero, subtract } from dinero.js; import { USD } from dinero.js/currencies; const d1 dinero({ amount: 500, currency: USD }); const d2 dinero({ amount: 1000, currency: USD, scale: 3 }); subtract(d1, d2); // amount 4000scale 3这里500scale 2先被归一化为5000scale 3再减去1000得到4000。批量求和/求差add与subtract都是二元运算接受两个参数对应源码中的AddParams/SubtractParams元组类型。要处理多个对象可以多次调用或使用reduce组合const d1 dinero({ amount: 300, currency: USD }); const d2 dinero({ amount: 200, currency: USD }); const d3 dinero({ amount: 100, currency: USD }); const addMany (addends) addends.reduce(add); addMany([d1, d2, d3]); // amount 600const s1 dinero({ amount: 400, currency: USD }); const s2 dinero({ amount: 200, currency: USD }); const s3 dinero({ amount: 100, currency: USD }); const subtractMany (subtrahends) subtrahends.reduce(subtract); subtractMany([s1, s2, s3]); // amount 100从源码类型可以看出add的第一个参数augend与第二个参数addend都必须是 Dinero 对象core/api/add.tssubtract对应minuend被减数与subtrahend减数见 core/api/subtract.ts。深入multiply乘法器与最安全 scalemultiply用于把一个 Dinero 对象乘以某个数值。它的参数multiplier有两种形式见 docs/api/mutations/multiply.md 的参数表整数如4带刻度的数量scaled amount如{ amount: 21, scale: 1 }表示 2.1。为什么小数乘法要用 scaled amount原文档明确警告如果需要乘以小数fractional multiplier不要使用浮点数而要使用 scaled amounts。例如要乘以 2.1应传{ amount: 21, scale: 1 }而不是2.1。原因与 Dinero.js 一贯的精度策略一致浮点数如 0.1 0.2在二进制表示下存在精度误差而整数运算可以避免这类误差。整数乘法示例import { dinero, multiply } from dinero.js; import { USD } from dinero.js/currencies; const d dinero({ amount: 400, currency: USD }); multiply(d, 4); // amount 1600scaled multiplier 与 scale 相加规则import { dinero, multiply } from dinero.js; import { USD } from dinero.js/currencies; const d dinero({ amount: 401, currency: USD }); multiply(d, { amount: 2001, scale: 3 }); // amount 802401scale 5注意结果 scale 变为 5因为对象的 scale2与 multiplier 的 scale3相加得到 5。这一逻辑直接体现在源码 core/api/multiply.ts 中const { amount: multiplierAmount, scale: multiplierScale } getAmountAndScale(multiplier, zero); const newScale calculator.add(scale, multiplierScale); return convertScaleFn( multiplicand.create({ amount: calculator.multiply(amount, multiplierAmount), currency, scale: newScale, }), newScale );其中getAmountAndScale负责把整数或 scaled amount 统一抽取为金额 scale若传入整数其 scale 视为 0工具函数见 core/utils/index.ts 下的getAmountAndScale。随后convertScaleFn即transformScale会把结果转换到最安全的 scale避免出现无法整除造成精度损失的情况。关于安全 scale 的算法细节可参考 docs/api/conversions/transform-scale.md。深入allocate按比例分配与余数摊派allocate把一个 Dinero 对象的金额按一组比例ratios拆分到多个新对象上。货币的最小单位不可再分因此金额不一定能被精确均分——allocate的职责就是拆分后把余数尽可能公平地分配出去。百分比与比值两种写法等价你可以用百分比风格也可以用比值风格两者等价[25, 75]与[1, 3]效果相同。import { dinero, allocate } from dinero.js; import { USD } from dinero.js/currencies; const d dinero({ amount: 500, currency: USD }); const [d1, d2] allocate(d, [50, 50]); // d1: amount 250d2: amount 250const d dinero({ amount: 100, currency: USD }); const [d1, d2] allocate(d, [1, 3]); // d1: amount 25d2: amount 75余数如何被尽可能公平地摊派当金额无法被比例整除时余数会按比例大小降序依次分配给各份。原文档的示例const d dinero({ amount: 1003, currency: USD }); const [d1, d2] allocate(d, [50, 50]); // d1: amount 502d2: amount 5011003 无法被平分余数 1 被分配给第一份得到 502 与 501。这一行为由distribute工具函数实现见 core/utils/distribute.tslet remainder value; const shares ratios.map((ratio) { const share calculator.integerDivide(calculator.multiply(value, ratio), total) || zero; remainder calculator.subtract(remainder, share); return share; }); // ... // 按比例降序排序索引余数依次 1 分配给比例较大的份额 const sortedIndices ratios .map((ratio, index) ({ ratio, index })) .filter(({ ratio }) !equalFn(ratio, zero)) .sort((a, b) (greaterThanFn(a.ratio, b.ratio) ? -1 : 1)) .map(({ index }) index);也就是说先按value × ratio / total做整数除法得到基础份额余数则按比例大的优先逐一分发每次 1。源码中还包含一个针对浮点精度损失的防死循环保护if (equalFn(newRemainder, remainder)) break;这在使用 number calculator 且金额超过Number.MAX_SAFE_INTEGER时尤为重要。支持零比例你可以传入零比例例如[0, 50, 50]。如果存在需要分配的余数零比例会被跳过返回 amount 为 0 的对象const d dinero({ amount: 1003, currency: USD }); const [d1, d2, d3] allocate(d, [0, 50, 50]); // d1: amount 0 // d2: amount 502 // d3: amount 501合法比例的两个硬性约束原文档强调两条规则所有比例必须为正且不能只传零比例。这两条约束在源码 core/api/allocate.ts 中被编码为断言条件const hasOnlyPositiveRatios normalizedRatios.every(({ amount }) greaterThanOrEqualFn(amount, zero) ); const hasOneNonZeroRatio normalizedRatios.some(({ amount }) greaterThanFn(amount, zero) ); const condition hasRatios hasOnlyPositiveRatios hasOneNonZeroRatio; assert(condition, INVALID_RATIOS_MESSAGE);不满足时抛出Ratios are invalid.见 core/checks/messages.ts。注意hasOnlyPositiveRatios使用greaterThanOrEqual即允许 0但至少需要一个严格大于 0 的比例。小数比例同样使用 scaled amounts与multiply一致allocate也要求小数比例使用 scaled amounts而不是浮点数。例如 50.5% 与 49.5% 应写成import { dinero, allocate } from dinero.js; import { USD } from dinero.js/currencies; const ratios [ { amount: 505, scale: 1 }, { amount: 495, scale: 1 }, ]; // 等价于比例 50.5 和 49.5 const d dinero({ amount: 100, currency: USD }); const [d1, d2] allocate(d, ratios); // d1: amount 505scale 3 // d2: amount 495scale 3这里返回对象的 scale 变为 3来自对象的 scale2与比例最高 scale1相加newScale scale highestRatioScale见 core/api/allocate.ts随后同样会转换到最安全 scale。内部实现会先把所有比例归一化到同一 scale按最高比例 scale 对齐用power(ten, factor)补足倍数再交给distribute计算份额。组合使用与相关资源一个综合示例把add、subtract、allocate串起来就是一个完整的打折 均摊流水线import { dinero, add, allocate, subtract } from dinero.js; import { USD } from dinero.js/currencies; const base dinero({ amount: 1003, currency: USD }); const [partA, partB] allocate(base, [50, 50]); // 502 / 501 const fee dinero({ amount: 99, currency: USD }); const totalForA add(partA, fee); // 601 const finalForB subtract(partB, fee); // 402由于所有函数都返回新对象且不修改入参你可以放心地把每一步结果作为下一步的输入形成清晰的声明式计算链。深入阅读指引API 参考每个 mutation 函数的参数表与更多示例见 docs/api/mutations/add.md、docs/api/mutations/subtract.md、docs/api/mutations/multiply.md、docs/api/mutations/allocate.md核心概念金额与精度约定见 docs/core-concepts/amount.md、docs/core-concepts/scale.md比较类运算见 docs/core-concepts/comparisons.md源码实现各函数核心逻辑位于 packages/dinero.js/src/core/api余数摊派算法见 packages/dinero.js/src/core/utils/distribute.ts错误消息见 packages/dinero.js/src/core/checks/messages.ts测试用例add、subtract、multiply、allocate的单元测试分别位于 packages/dinero.js/src/api/tests/add.test.ts、packages/dinero.js/src/api/tests/subtract.test.ts、packages/dinero.js/src/api/tests/multiply.test.ts、packages/dinero.js/src/api/tests/allocate.test.ts可用于验证本文描述的各种边界行为实战参考仓库中的 examples/cart-react 与 examples/cart-vue 示例项目展示了如何在真实购物车场景中组合这些 mutation 函数。小结mutation 函数虽名为变更实则返回新对象add、subtract、multiply、allocate都不会修改入参这是 Dinero.js 不可变设计immutability的核心同币种是加减法的硬性前提add/subtract在运行时断言币种一致TypeScript 类型化货币下还能在编译期拦截不同 scale 自动归一化加减法会统一到最高 scale 后再计算乘法与分配则采用scale 相加 转换到最安全 scale的策略避免浮点使用 scaled amounts无论是小数乘法器还是小数比例都应写成{ amount, scale }形式allocate会把余数公平摊派按比例降序分配余数、支持零比例但要求全为正且至少一个非零。掌握这些规则后你就能在结算、分摊、折扣、报表等场景中安全地组合 Dinero.js 的 mutation API写出既精确又易于维护的金额计算代码。赞分享金融科技【免费下载链接】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 金额比较greaterThanOrEqual 函数使用指南与实现原理dinero.js 金额比较greaterThanOrEqual 函数使用指南与实现原理 greaterThanOrEqual 是 dinero.js 提供的金融科技OpenCloud 中的环境变量加载利器深入解析 gotenv 的变更历史与源码实现OpenCloud 中的环境变量加载利器深入解析 gotenv 的变更历史与源码实现 导读 gotenv 是 OpenCloud 项目中用于从 .env 文件后端微服务存储认证鉴权上一篇如何免费升级旧Mac到最新macOSOpenCore Legacy Patcher终极指南下一篇通达信数据读取的三大痛点与mootdx解决方案深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考