value-objects-money-and-units
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseValue objects: money & units
Value Object:金额与单位
Store every quantity once, in one canonical form, and convert only when shown or
exported. Money is integer minor units plus a currency; physical quantities are
whole SI base units; time is UTC. This is what lets a user flip any display
preference (currency symbol, unit system, locale) without corrupting one stored
row, and lets "sum of parts == whole" be structural, not hoped-for.
This core is pure Dart — no , no , no , no plugins.
Formatting and digit normalization happen upstream/downstream, not here. These
value objects live in , the sanctioned pure-foundation layer (see
) — never a // grab-bag.
flutter/*intldart:iolib/core/project-structure-and-packagesutils/common/shared/Read the reference for the task at hand:
- — the largest-remainder
references/allocate-and-splitting.mdprimitive, its invariants, the two-rounding-sites trap, edge policies, verified test vectors, and the subtotal→tax→tip split pipeline.allocate() - — the ISO-4217 exponent rule, the SI unit tables,
references/canonical-storage.md-based parsing, the cents-accumulator for keypad input, rounding discipline, and the Clock-injected dated-rate / staleness engine.decimal - — value-type modelling: relationships as id links, derive-don't-store, one currency per aggregate, immutable state with value equality.
references/domain-model.md
Run and before a PR.
scripts/check-money-violations.shscripts/verify-core.sh每个数量只应存储一次,并统一采用一种规范形态,仅在展示或
导出时进行转换。Money 由整数最小货币单位和 Currency 组成;物理量使用完整的 SI 基本单位;时间使用 UTC。正因如此,用户无论切换任何展示偏好(货币符号、单位制、locale),都不会破坏任何一行存储的数据,也能让“各部分之和 == 整体”成为结构性保证,而不是仅靠期望。
此核心是纯 Dart——不包含 、、 或任何 plugin。格式化和数字规范化在上游或下游完成,而不在此核心中完成。这些 Value Object 位于 ,即经过认可的纯基础层(参见 ),绝不能塞进 // 这种大杂烩目录。
flutter/*intldart:iolib/core/project-structure-and-packagesutils/common/shared/阅读与当前任务相关的参考资料:
- ——largest-remainder
references/allocate-and-splitting.md原语、其不变量、两个舍入点陷阱、边界策略、经过验证的测试向量,以及 subtotal→tax→tip 拆分流水线。allocate() - ——ISO-4217 指数规则、SI 单位表、基于
references/canonical-storage.md的解析、用于键盘输入的 cents accumulator、舍入规范,以及注入 Clock 的 dated-rate/staleness engine。decimal - ——Value Type 建模:关系采用 id 链接、derive-don't-store、每个 aggregate 仅使用一种 Currency,以及带 value equality 的不可变状态。
references/domain-model.md
提交 PR 前运行 和 。
scripts/check-money-violations.shscripts/verify-core.shNon-negotiable rules
不可妥协的规则
- Money is minor units + a
int— NEVERCurrency/double/num. Binary floats cannot representREAL; drift silently corrupts totals the user can never re-derive. No money API accepts or returns0.01.double - Derive minor-units-per-major from the currency's real ISO-4217 exponent —
NEVER hardcode ,
* 100, or "2 decimals". Exponent is 0 for JPY/VND, 2 for USD/EUR, 3 for KWD/BHD/OMR. A hardcoded/ 100is a 100× error for a 0-exponent currency and a 10× error for a 3-exponent one. Route through100.currency.minorPerMajor - Unknown currency code is a typed failure, never a silent default-to-2. The
exponent table lists only shipped currencies; returns null and the caller emits a
Currency.tryParse.Failure - One currency per aggregate; cross-currency arithmetic is forbidden. Adding
two of different currencies is a category error — throw (programmer error) or convert through the FX layer first. Keep currency a fact of the enclosing aggregate so
Moneyarithmetic never has to guard it.Money - Route EVERY division of money through one primitive. Shared items, tax proration, tip proration, discounts — all one rounding path, so "parts sum to the whole to the exact minor unit" is proven once and tested once.
allocate(amount, weights) - There are TWO rounding sites, not one: AND percent→minor-units. Round a percentage to integer minor units once before feeding
allocate(). A naiveallocate()percent is a classic off-by-a-cent bug thatdoublecoverage will not catch.allocate - Never sum independently-rounded parts to get a total. Always a known integer total and let the parts absorb the residual.
allocate() - Derive totals; never store them. A denormalized stored total is the classic drift bug. Totals are computed from items + weights on read.
- Model relationships as stable-links, not embedded copies. Give every entity an explicit
id(e.g. a UUID). Editing a price then leaves assignments intact and deleting a participant just drops them from link sets.final id - Store canonically, convert at the edge. Physical quantities are whole SI
base units (metres / millilitres / minutes); time is a UTC
int.DateTimefactories round into canonical;.from<DisplayUnit>getters return ato<DisplayUnit>()used only at the presentation edge.double - Normalize digits/separators to ASCII BEFORE input reaches this core. Never
call /
int.parse/double.parseon raw localized input — it throws on Eastern-Arabic numerals. Fold upstream (seeDecimal.parse).i18n-rtl-l10n - Parse with ; round ONCE with an explicit mode at the boundary. Use
decimalfor exact division/parsing, apply an explicitpackage:decimal(default half-even/banker's) once at the parse or final-total boundary — never on intermediate sums (accumulates bias).RoundingMode - Inject 's
package:clock; NEVER callClock, never roll a bespokeDateTime.now(). Every time-reading class in this pure core takes aClockServiceconstructor arg; Riverpod/feature code injects the sameClockthrough aClock(seeclockProvider) so the two vocabularies compose. Fixed clocks /service-boundary-and-nativethen make time-dependent logic deterministic in tests.fake_async - The core stays Flutter-free and IO-free. Deps are only and
decimal. Formatting lives in the presentation layer; storage in the data layer.clock
- Money 是 最小货币单位 +
int——绝不使用Currency/double/num。 二进制浮点数无法表示REAL;这种漂移会悄无声息地破坏总额,而且用户之后永远无法重新推导出来。金额 API 不接受或返回0.01。double - 根据 Currency 真实的 ISO-4217 指数推导每个主单位对应的最小货币单位——绝不硬编码 、
* 100或“2 位小数”。 JPY/VND 的指数为 0,USD/EUR 为 2,KWD/BHD/OMR 为 3。对 0 指数货币硬编码/ 100会造成 100 倍误差,对 3 指数货币则会造成 10 倍误差。必须通过100完成换算。currency.minorPerMajor - 未知 Currency 代码必须产生类型化失败,绝不能静默默认为 2 位小数。
指数表只包含已发布的 Currency;返回 null,由调用方生成一个
Currency.tryParse。Failure - 每个 aggregate 仅使用一种 Currency;禁止跨币种运算。 对两个 Currency 不同的 做加法属于类别错误——应抛出异常(程序员错误),或先通过 FX 层转换。Currency 始终作为外层 aggregate 的一项事实,使
Money运算无需自行校验。Money - 金额的所有除分和拆分都必须通过同一个 原语。 共享项目、税费分摊、小费分摊、折扣——全部走同一条舍入路径,从而只需证明并测试一次“各部分之和精确等于整体的最小货币单位”。
allocate(amount, weights) - 存在两个舍入点,而不是一个:和百分比→最小货币单位。 将百分比舍入为整数最小货币单位且只舍入一次,然后再传给
allocate()。直接使用allocate()百分比是典型的差一分钱问题,即使double的测试覆盖充分也无法捕获。allocate - 绝不对分别舍入后的部分求和来得到总额。 始终对一个已知的整数总额执行 ,并让各部分吸收余数。
allocate() - 推导总额,绝不存储总额。 反规范化存储的总额是典型的漂移问题。读取时根据项目和权重计算总额。
- 将关系建模为稳定 链接,而不是嵌入副本。 为每个实体提供明确的
id(例如 UUID)。修改价格后,已有分配关系保持不变;删除参与者时,只需将其从链接集合中移除。final id - 规范存储,在边界转换。 物理量使用完整的 SI 基本单位(米/毫升/分钟);时间使用 UTC
int。DateTime工厂负责舍入转入规范形态;.from<DisplayUnit>getter 返回仅供展示边界使用的to<DisplayUnit>()。double - 在输入进入此核心之前,将数字和分隔符规范化为 ASCII。 绝不要对原始本地化输入调用 /
int.parse/double.parse——遇到东阿拉伯数字时它们会抛出异常。应在上游完成折叠处理(参见Decimal.parse)。i18n-rtl-l10n - 使用 解析;在边界使用明确的模式且只舍入一次。 使用
decimal进行精确除法和解析,在解析或最终总额边界应用明确的package:decimal(默认采用 half-even/银行家舍入)一次,绝不要对中间求和结果进行舍入,否则会累积偏差。RoundingMode - 注入 的
package:clock;绝不调用Clock,也绝不自行实现DateTime.now()。 纯核心中的每个时间读取类都在构造函数中接收ClockService;Riverpod/feature 代码通过Clock注入同一个clockProvider(参见Clock),使两套 Clock 术语体系能够组合。固定 Clock 或service-boundary-and-native随后可让依赖时间的逻辑在测试中保持确定性。fake_async - 核心必须与 Flutter 和 IO 无关。 依赖项只有 和
decimal。格式化位于展示层,存储位于数据层。clock
Canonical storage
Canonical storage
Widgets and repositories exchange value objects, never raw s or strings.
int| Quantity | Canonical storage | Type | Never store |
|---|---|---|---|
| Money | integer minor units + | | double, formatted string |
| Distance | whole metres | | km, miles |
| Volume | whole millilitres | | litres, gallons |
| Duration | whole minutes (or | | hours as double |
| Timestamp | UTC ISO-8601 instant | | local time |
dart
/// ISO-4217 minor-unit exponents. Explicit table — NEVER default to 2.
enum Currency {
jpy('JPY', 0), vnd('VND', 0),
usd('USD', 2), eur('EUR', 2), gbp('GBP', 2),
kwd('KWD', 3), bhd('BHD', 3), omr('OMR', 3);
const Currency(this.code, this.exponent);
final String code;
final int exponent;
/// 10^exponent — minor units per major unit. The ONLY scaling source.
int get minorPerMajor => switch (exponent) {
0 => 1,
2 => 100,
3 => 1000,
_ => throw StateError('unsupported exponent $exponent for $code'),
};
static Currency? tryParse(String code) {
// Plain loop, not `firstOrNull` (a package:collection extension) — keeps the
// core dependency-free beyond `decimal`.
for (final c in Currency.values) {
if (c.code == code) return c;
}
return null;
}
}
/// Money is (integer minor units) + (currency). No floats, ever.
final class Money implements Comparable<Money> {
const Money(this.minorUnits, this.currency);
final int minorUnits; // e.g. 12345 with KWD == 12.345 KWD
final Currency currency;
Money operator +(Money o) => currency == o.currency
? Money(minorUnits + o.minorUnits, currency)
: throw ArgumentError('currency mismatch: $currency vs ${o.currency}');
int compareTo(Money o) {
assert(currency == o.currency, 'compare across currencies is a bug');
return minorUnits.compareTo(o.minorUnits);
}
bool operator ==(Object o) =>
o is Money && o.minorUnits == minorUnits && o.currency == currency;
int get hashCode => Object.hash(minorUnits, currency);
}Physical value objects follow the identical shape — an integer canonical field,
rounding factories, and edge-only getters. See
and .
.from<Unit>to<Unit>()references/canonical-storage.mdexamples/money.dartWidget 和 repository 交换的是 Value Object,而不是原始 或字符串。
int| 数量 | 规范存储 | 类型 | 绝不可存储 |
|---|---|---|---|
| Money | 整数最小货币单位 + | | double、格式化字符串 |
| Distance | 完整的米 | | km、miles |
| Volume | 完整的毫升 | | litres、gallons |
| Duration | 完整的分钟(或 | | 以 double 表示的小时 |
| Timestamp | UTC ISO-8601 时刻 | | 本地时间 |
dart
/// ISO-4217 minor-unit exponents. Explicit table — NEVER default to 2.
enum Currency {
jpy('JPY', 0), vnd('VND', 0),
usd('USD', 2), eur('EUR', 2), gbp('GBP', 2),
kwd('KWD', 3), bhd('BHD', 3), omr('OMR', 3);
const Currency(this.code, this.exponent);
final String code;
final int exponent;
/// 10^exponent — minor units per major unit. The ONLY scaling source.
int get minorPerMajor => switch (exponent) {
0 => 1,
2 => 100,
3 => 1000,
_ => throw StateError('unsupported exponent $exponent for $code'),
};
static Currency? tryParse(String code) {
// Plain loop, not `firstOrNull` (a package:collection extension) — keeps the
// core dependency-free beyond `decimal`.
for (final c in Currency.values) {
if (c.code == code) return c;
}
return null;
}
}
/// Money is (integer minor units) + (currency). No floats, ever.
final class Money implements Comparable<Money> {
const Money(this.minorUnits, this.currency);
final int minorUnits; // e.g. 12345 with KWD == 12.345 KWD
final Currency currency;
Money operator +(Money o) => currency == o.currency
? Money(minorUnits + o.minorUnits, currency)
: throw ArgumentError('currency mismatch: $currency vs ${o.currency}');
int compareTo(Money o) {
assert(currency == o.currency, 'compare across currencies is a bug');
return minorUnits.compareTo(o.minorUnits);
}
bool operator ==(Object o) =>
o is Money && o.minorUnits == minorUnits && o.currency == currency;
int get hashCode => Object.hash(minorUnits, currency);
}物理量 Value Object 采用完全相同的结构——一个整数规范字段、会对规范形态进行舍入的 工厂,以及仅在边界使用的 getter。参见 和 。
.from<Unit>to<Unit>()references/canonical-storage.mdexamples/money.dartThe one division path: allocate()
allocate()唯一的拆分路径:allocate()
allocate()Every time money is split, it goes through this integer largest-remainder
(Hamilton) primitive. Never divide money any other way.
dart
/// Splits [amount] minor units across [weights], guaranteeing the parts sum
/// EXACTLY to [amount]. Deterministic ascending-index tie-break; residual < n.
/// Negative amount mirrors and negates (discounts/refunds); zero weight-sum
/// falls back to equal weights; empty weights returns [] — money math must
/// never throw into the UI.
List<int> allocate(int amount, List<int> weights) {
final n = weights.length;
if (n == 0) return const [];
if (amount < 0) return allocate(-amount, weights).map((s) => -s).toList();
final sanitized = [for (final w in weights) w < 0 ? 0 : w];
final weightSum = sanitized.fold(0, (a, b) => a + b);
final w = weightSum == 0 ? List.filled(n, 1) : sanitized;
final total = weightSum == 0 ? n : weightSum;
final shares = List<int>.filled(n, 0);
final remainders = <({int remainder, int index})>[];
var distributed = 0;
for (var i = 0; i < n; i++) {
final product = amount * w[i]; // multiply FIRST, then ~/ and % — no float
final floorShare = product ~/ total;
shares[i] = floorShare;
distributed += floorShare;
remainders.add((remainder: product % total, index: i));
}
var leftover = amount - distributed; // always in 0 ..< n
remainders.sort((a, b) => a.remainder != b.remainder
? b.remainder.compareTo(a.remainder)
: a.index.compareTo(b.index));
for (var k = 0; leftover > 0; k++, leftover--) {
shares[remainders[k].index] += 1;
}
assert(shares.fold(0, (a, b) => a + b) == amount, 'allocate must be exact');
return shares;
}Verified vectors (assert these in a test):
·
·
.
allocate(1001,[1,1,1]) == [334,334,333]allocate(660,[1584,4033,1933]) == [138,353,169]allocate(1510,[1584,4033,1933]) == [317,807,386]The split pipeline layers in passes — item subtotals, then
, then — so every
whole-bill figure is distributed and the per-participant finals sum to the grand
total exactly. Full pipeline, edge policies, and the percent→minor-units rounding
site are in and .
allocate()allocate(taxMinor, subtotals)allocate(tipMinor, subtotals)references/allocate-and-splitting.mdexamples/allocate.dart每次拆分金额,都必须使用这个基于整数的 largest-remainder(Hamilton)原语。绝不能以其他任何方式对金额进行除分。
dart
/// Splits [amount] minor units across [weights], guaranteeing the parts sum
/// EXACTLY to [amount]. Deterministic ascending-index tie-break; residual < n.
/// Negative amount mirrors and negates (discounts/refunds); zero weight-sum
/// falls back to equal weights; empty weights returns [] — money math must
/// never throw into the UI.
List<int> allocate(int amount, List<int> weights) {
final n = weights.length;
if (n == 0) return const [];
if (amount < 0) return allocate(-amount, weights).map((s) => -s).toList();
final sanitized = [for (final w in weights) w < 0 ? 0 : w];
final weightSum = sanitized.fold(0, (a, b) => a + b);
final w = weightSum == 0 ? List.filled(n, 1) : sanitized;
final total = weightSum == 0 ? n : weightSum;
final shares = List<int>.filled(n, 0);
final remainders = <({int remainder, int index})>[];
var distributed = 0;
for (var i = 0; i < n; i++) {
final product = amount * w[i]; // multiply FIRST, then ~/ and % — no float
final floorShare = product ~/ total;
shares[i] = floorShare;
distributed += floorShare;
remainders.add((remainder: product % total, index: i));
}
var leftover = amount - distributed; // always in 0 ..< n
remainders.sort((a, b) => a.remainder != b.remainder
? b.remainder.compareTo(a.remainder)
: a.index.compareTo(b.index));
for (var k = 0; leftover > 0; k++, leftover--) {
shares[remainders[k].index] += 1;
}
assert(shares.fold(0, (a, b) => a + b) == amount, 'allocate must be exact');
return shares;
}经过验证的测试向量(在测试中断言这些结果):
·
·
。
allocate(1001,[1,1,1]) == [334,334,333]allocate(660,[1584,4033,1933]) == [138,353,169]allocate(1510,[1584,4033,1933]) == [317,807,386]拆分流水线会分层执行 ——先处理项目小计,再执行 ,最后执行 ——因此每个整单金额都会被分配,且各参与者最终金额之和精确等于总金额。完整流水线、边界策略和百分比→最小货币单位的舍入点参见 和 。
allocate()allocate(taxMinor, subtotals)allocate(tipMinor, subtotals)references/allocate-and-spliting.mdexamples/allocate.dartParsing input to exact minor units
将输入解析为精确的最小货币单位
Input must already be ASCII-normalized upstream. Parse with (no binary
error), scale by the currency's exponent, round once.
decimaldart
/// Caller MUST have normalized digits + separators to ASCII first.
Money moneyFromMajorString(String ascii, Currency c) {
final scaled = (Decimal.parse(ascii) * Decimal.fromInt(c.minorPerMajor))
.round(); // exact; Decimal has no binary-float error
return Money(scaled.toBigInt().toInt(), c);
}For a numeric keypad with no fixed decimal key, accumulate digits into minor
units directly and never touch a locale decimal separator — see
.
references/canonical-storage.md输入必须已在上游完成 ASCII 规范化。使用 解析(不产生二进制误差),按 Currency 指数缩放,并只舍入一次。
decimaldart
/// Caller MUST have normalized digits + separators to ASCII first.
Money moneyFromMajorString(String ascii, Currency c) {
final scaled = (Decimal.parse(ascii) * Decimal.fromInt(c.minorPerMajor))
.round(); // exact; Decimal has no binary-float error
return Money(scaled.toBigInt().toInt(), c);
}对于没有固定小数点按键的数字键盘,应直接将数字累加到最小货币单位中,绝不能接触 locale 小数分隔符——参见 。
references/canonical-storage.mdDerive, don't store; inject a Clock
Derive-don't-store;注入 Clock
Totals are computed on read from items + weights; a stored total is a drift bug.
Any time-dependent value object (a dated rate, a staleness band, an expiry) takes
an injected so tests are deterministic.
Clockdart
final class RateSnapshot {
const RateSnapshot(this._clock);
final Clock _clock;
int ageDays(DateTime asOfUtc) => _clock.now().difference(asOfUtc).inDays;
}
// prod: RateSnapshot(const Clock()); test: RateSnapshot(Clock.fixed(fixedUtc));读取时根据项目和权重计算总额;存储总额是漂移问题。任何依赖时间的 Value Object(带日期的汇率、staleness band、到期时间)都接收注入的 ,从而让测试保持确定性。
Clockdart
final class RateSnapshot {
const RateSnapshot(this._clock);
final Clock _clock;
int ageDays(DateTime asOfUtc) => _clock.now().difference(asOfUtc).inDays;
}
// prod: RateSnapshot(const Clock()); test: RateSnapshot(Clock.fixed(fixedUtc));Anti-patterns
反模式
- /
double amountfields. Cannot representnum price; corrupts totals irreversibly. Use0.01minor units.int - /
amount * 100. Wrong for every non-2-exponent currency. Route throughcents / 100.currency.minorPerMajor - Defaulting an unknown currency to 2 decimals. Silently mis-scales. Return a typed failure.
- Adding across currencies, or storing a currency per line item when the whole aggregate is single-currency. Convert at the FX boundary; keep currency at the aggregate level.
Money - Summing independently-rounded shares to produce a total. Rounds twice and
drifts. a known integer total instead.
allocate() - Recomputing tax/tip from a percentage inside the allocation loop. Round the
percent to integer minor units once, then .
allocate() - Storing a denormalized on the entity. Derive it.
total - , or a hand-rolled
DateTime.now()/ClockService/SystemClock, inside the pure core. Untestable / non-composable time. InjectFakeClock'spackage:clock.Clock - or
Decimal(someDouble)on raw localized input. Binary error / throws on Eastern digits. Parseint.parsefrom aDecimal; normalize first.String - Rounding intermediate sums. Accumulates bias. Round once at the boundary.
- /
double amount字段。 无法表示num price;会不可逆地破坏总额。使用0.01最小货币单位。int - /
amount * 100。 对每个非 2 指数 Currency 来说都是错误的。必须通过cents / 100处理。currency.minorPerMajor - 将未知 Currency 默认设为 2 位小数。 这会静默地错误缩放。必须返回类型化失败。
- 跨币种添加 ,或者在整个 aggregate 使用单一 Currency 时仍为每个行项目存储 Currency。 应在 FX 边界转换;Currency 保持在 aggregate 层级。
Money - 对分别舍入的份额求和来生成总额。 这会舍入两次并产生漂移。应对已知的整数总额执行 。
allocate() - 在分配循环内部根据百分比重新计算税费/小费。 先将百分比舍入为整数最小货币单位一次,然后再执行 。
allocate() - 在实体上存储反规范化的 。 应通过计算得出。
total - 在纯核心中调用 ,或自行实现
DateTime.now()/ClockService/SystemClock。 时间将无法测试,也无法组合。注入FakeClock的package:clock。Clock - 使用 ,或对原始本地化输入调用
Decimal(someDouble)。 会产生二进制误差,或在东阿拉伯数字上抛出异常。应从int.parse解析String;先进行规范化。Decimal - 对中间求和结果进行舍入。 会累积偏差。只在边界舍入一次。
Definition of done
完成标准
- Every money field is minor units + a
int; noCurrency/double.num - All scaling goes through ; no literal
currency.minorPerMajor.100 - Unknown currency returns a typed failure, never a defaulted parse.
- No cross-currency arithmetic; currency lives at the aggregate level.
- Every money split calls ; conservation asserted and tested with the verified vectors.
allocate() - Percentages round to integer minor units once, before .
allocate() - No stored totals; totals derive on read.
- Entities carry an explicit stable ; relationships are id links.
id - Physical quantities stored as SI s; converted only at the edge.
int - Time-reading classes take an injected ; no
Clockin core.DateTime.now() - Core imports only and
decimal; no Flutter/intl/dart:io.clock - and
scripts/check-money-violations.shpass.scripts/verify-core.sh
- 每个金额字段均为 最小货币单位 +
int;不使用Currency/double。num - 所有缩放都通过 完成;不出现字面量
currency.minorPerMajor。100 - 未知 Currency 返回类型化失败,绝不默认解析。
- 不进行跨币种运算;Currency 位于 aggregate 层级。
- 每次金额拆分都调用 ;使用经过验证的测试向量断言并测试守恒性。
allocate() - 百分比在 之前只舍入为整数最小货币单位一次。
allocate() - 不存储总额;读取时推导总额。
- 实体携带明确且稳定的 ;关系采用 id 链接。
id - 物理量以 SI 存储;仅在边界转换。
int - 时间读取类接收注入的 ;核心中不调用
Clock。DateTime.now() - 核心仅导入 和
decimal;不包含 Flutter/intl/dart:io。clock - 和
scripts/check-money-violations.sh通过。scripts/verify-core.sh
Related skills
相关 skill
- See for where this pure core lives (
project-structure-and-packages, the sanctioned foundation layer) within the feature-first app layout.lib/core/ - See for injecting this
service-boundary-and-nativeinto Riverpod code viaClock(the sameclockProviderseam, provider-wired).package:clock - See for the sealed
error-handling-typed-resultsspine that parsing and FX return instead of throwing.Result<T, F extends Failure> - See for ASCII digit/separator normalization and currency formatting at the presentation edge (kept out of this pure core).
i18n-rtl-l10n - See for immutable value types, sealed types, and total non-throwing domain functions.
dart3-idioms-and-coding-standards - See for storing minor units + ISO code (never a REAL) and mapping rows to these value objects.
persistence-drift - See for the clock-injected, table-driven unit tests these pure functions demand.
testing-strategy
- 参见 ,了解此纯核心在 feature-first app 布局中的位置(
project-structure-and-packages,经过认可的基础层)。lib/core/ - 参见 ,了解如何通过
service-boundary-and-native将此clockProvider注入 Riverpod 代码(使用相同的Clockseam,并由 provider 连接)。package:clock - 参见 ,了解 sealed
error-handling-typed-results主干;解析和 FX 返回该类型,而不是抛出异常。Result<T, F extends Failure> - 参见 ,了解 ASCII 数字/分隔符规范化,以及位于展示边界的 Currency 格式化(这些逻辑不进入纯核心)。
i18n-rtl-l10n - 参见 ,了解不可变 Value Type、sealed type,以及完全非抛异常的 domain function。
dart3-idioms-and-coding-standards - 参见 ,了解如何存储最小货币单位 + ISO 代码(绝不存储 REAL),并将数据行映射到这些 Value Object。
persistence-drift - 参见 ,了解这些纯函数所需的、注入 Clock 且采用 table-driven 风格的单元测试。
testing-strategy
References
参考资料
- ISO 4217 currency exponents — https://en.wikipedia.org/wiki/ISO_4217
- — https://pub.dev/packages/decimal
package:decimal - — https://pub.dev/packages/clock
package:clock - — https://pub.dev/packages/fake_async
fake_async - Largest-remainder (Hamilton) method — https://en.wikipedia.org/wiki/Largest_remainder_method
- Dart records & patterns — https://dart.dev/language/records
- ISO 4217 Currency exponent — https://en.wikipedia.org/wiki/ISO_4217
- — https://pub.dev/packages/decimal
package:decimal - — https://pub.dev/packages/clock
package:clock - — https://pub.dev/packages/fake_async
fake_async - Largest-remainder(Hamilton)method — https://en.wikipedia.org/wiki/Largest_remainder_method
- Dart records & patterns — https://dart.dev/language/records