SuperAPI AI 工具集

value-objects-money-and-units

编程与开发

查看项目

更新于 10-18 18:02

技能介绍

本技能面向 Expo 应用,解决金额、物理量和时刻的存储与转换问题。核心原则是每种值只以一种规范整数形式存储一次,只由纯函数、经过测试的运算修改,只在边缘层转成文本显示,从而避免语言、数字系统或单位制切换破坏已存储的数据。技能提供两个零依赖检查器,在应用自身代码上验证规则。

用途 适用于添加价格、货币、税费、分摊、单位或时间戳的场景,覆盖类型定义、状态、数据库行、API 交互与界面显示全链路。

金额规则 金额由最小货币单位的整数加 ISO 4217 货币代码组成(来自 src/core/money/money.ts 的 money(minor, currency)),普通数值字段命名为 <what>Minor。禁止浮点数、主单位数字或格式化字符串。换算倍数只能取 ISO 4217 指数表:JPY 为 0、USD 为 2、KWD 为 3、CLF 为 4 位小数;未知代码返回 unknown-currency。禁止 * 100、/ 100、toFixed(2) 或 ?? 2,也不使用 Intl/CLDR 的指数(有 16 个代码的显示位数与 ISO 不同,如 IQD 为 3 对 0)。

聚合内只用一种货币,算术只通过 Money 函数:addMoney、subtractMoney、sumMoney、compareMoney 在混合货币或不安全整数时抛错;换币使用带显式汇率的 convertMoney,只舍入一次。所有金额除法走 allocate()(最大余额法,余数相同取较低索引),用于账单拆分、按比例分摊税费或折扣、分期与均分,保证各部分之和精确等于总额。

汇率以精确比例在总额处应用一次,并指定舍入模式:parsePercent('8.25')、basisPoints(825)、applyRate(amount, rate, 'half-even')、含税价的 includedTax,然后用 allocate() 分配结果。禁止 amount * 0.0825,禁止对中间和舍入。

文本与第三方值在边界精确进入:输入先用 normalizeDecimalInput 按应用 locale 规范化,再由 parseMoney 按货币指数解析,多余小数位报 too-many-decimals 而非静默舍入,分组分隔符有歧义时报 ambiguous-separator。商店价格通过 moneyFromStoreDouble(expo-iap 的价格 double,双平台)或 moneyFromMicros(Android 订阅 micros)进入,展示给用户支付的价格使用商店自身的 displayPrice。

只在边缘层格式化,位于 src/i18n/:formatMoney(amount, localeTag) 或 useFormatMoney(),小数位按 ISO 指数设置,值以精确十进制文本传入。核心层不做任何格式化,不拼接货币符号。

数量与时刻 物理量以所在维度的标准单位整数表示,名称带单位(durationMs、distanceMm、weightG、volumeMl、sizeBytes);换算使用精确系数(1 英里 = 1,609,344 毫米),只舍入一次(half-even)到标准单位;单位制(expo-localization 的 measurementSystem 或设置项)只影响显示。

时刻是 UTC 毫秒时间戳,代码中为 <what>AtMs,SQLite 中为 <what>_at_ms INTEGER。ISO 8601 文本只存在于传输层,必须带 Z 或偏移量,由 parseIsoInstant 解析一次(无偏移文本被拒绝),由 toIsoInstant 写出;状态与存储中不使用 Date 对象或字符串;API 返回的秒数转换一次。日历日使用本地 DateKey。

持久化 SQL 金额列为 <name>_minor INTEGER NOT NULL,放在 STRICT 表中,货币作为聚合上的 TEXT;JSON 金额为带代码的十进制文本或最小单位整数,绝不用 JSON 浮点;schema 字段用 v.pipe(v.number(), v.safeInteger())。

核心与检查 src/core/{decimal,money,units,time}/ 只依赖 src/core(BigInt 算术,不引入 React Native、Expo 或 Intl),每个模块自带测试与固定向量。

工作流程:按任务查阅 references 下对应文档;检查前置条件(result.ts、带 FormatJS polyfill 的 i18n 层及 localeTagFor、渲染辅助);将 templates 复制到其首行指定的路径(总是复制 decimal 与 money,按需复制 units、time、i18n 相关文件),指数表只能删行不能改值;用 Money 字段建模领域,货币放在聚合上;在每个边界做一次转换(数据库行、迁移、API 响应、商店商品、输入框);用 applyRate 在总额处算一次再 allocate();用 useFormatMoney()、useLocaleTag() 和 formatQuantity 在组件之外生成最终字符串;改动规则先写测试,然后运行 npx jest src/core/decimal src/core/money src/core/units src/core/time src/i18n --ci --selectProjects unit 和 npm run typecheck;最后在仓库根目录运行 node ${CLAUDE_SKILL_DIR}/scripts/check-value-core.mjs . 与 node ${CLAUDE_SKILL_DIR}/scripts/check-value-code.mjs .,修复所有 FAIL 行直到两者都输出 RESULT: PASS。

完成标准包括:不存在浮点或主单位金额字段、列或 schema,所有 value-objects-allow 标记附带理由;换算、拆分和汇率均走核心函数;金额只由 src/i18n/ 的 formatMoney / useFormatMoney() 按 ISO 位数格式化;总额为派生值,存储的金额是记录事实并按要求命名;量与时刻为带单位后缀的整数;核心模块及其测试通过。

反模式示例:price: number 存 19.99;(priceMinor / 100).toFixed(2);EXPONENTS[code] ?? 2;对每人 Math.round(total / people);subtotal * 0.0825;分别舍入各部分再求和;在功能代码中 a.minor + b.minor;$${amount};对价格字段用 parseFloat;在行旁另存 total;用 Date 或 TEXT 存创建时间;无单位的 distance;用 formatMoney(productPrice) 显示应付价。

配套文件包括 references 下 6 篇参考文档、templates 核心与 i18n 模板及测试、examples 示例、scripts 检查脚本与 assets/iso-4217.json(ISO 4217 指数表,发布版本 2026-09-17,含 16 处 CLDR 差异)。相关技能为 error-handling-typed-results 与 i18n-strings-and-catalogs。

限制:资料未说明该技能是否收费、具体授权方式或对非 Expo 项目的支持情况,这些内容暂未提供。