Effect Schema 的 BigDecimal 比较校验:isGreaterThanBigDecimal 等五个高阶校验器详解
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本文基于
effect包 4.0.0 的 Schema 模块,围绕其新增的BigDecimal比较校验 API(isGreaterThanBigDecimal、isGreaterThanOrEqualToBigDecimal、isLessThanBigDecimal、isLessThanOrEqualToBigDecimal、isBetweenBigDecimal)展开。文章覆盖这五个校验器的语义、参数与边界行为,并结合源码解析其基于Order与makeFilter的底层实现、对fast-check任意值生成的约束推导,以及相应的测试用例,帮助读者在需要精确小数范围约束的场景(金额、利率、计量等)中正确使用 Effect Schema 完成运行时校验。
为什么需要 BigDecimal 校验器
JavaScript 的Number基于 IEEE 754 双精度浮点数,无法精确表示所有十进制小数,而金融、计量、科学计算等领域要求数值比较不丢失精度。Effect 为此提供了任意精度十进制类型BigDecimal(以value: bigint与scale: number表示,见 BigDecimal.ts 的make构造函数),但仅有数据类型还不够——Schema 还需要表达"金额必须大于 0""利率必须介于 0 到 1 之间"这类约束。
为此,Schema 模块在 4.0.0 中新增了一组针对BigDecimal的比较校验器,能够:
- 在解码(decode)阶段校验
BigDecimal值是否满足大于、大于等于、小于、小于等于、区间等约束; - 生成精确的期望(expected)错误消息,如
Expected a value greater than 1; - 为
fast-check属性测试推导符合约束的随机BigDecimal生成策略。
五个校验器 API 总览
以下五个校验器均定义在 Schema.ts,属于validation类别,自 4.0.0 起可用,全部以BigDecimal.Order作为比较依据:
| 校验器 | 语义 | 参数 | 边界 |
|---|---|---|---|
isGreaterThanBigDecimal(exclusiveMinimum) | 大于指定值(严格) | 单个BigDecimal | 不含下界 |
isGreaterThanOrEqualToBigDecimal(minimum) | 大于等于指定值 | 单个BigDecimal | 含下界 |
isLessThanBigDecimal(exclusiveMaximum) | 小于指定值(严格) | 单个BigDecimal | 不含上界 |
isLessThanOrEqualToBigDecimal(maximum) | 小于等于指定值 | 单个BigDecimal | 含上界 |
isBetweenBigDecimal({ minimum, maximum, exclusiveMinimum?, exclusiveMaximum? }) | 位于区间内 | 配置对象 | 默认两端包含,可分别排除 |
它们的用法完全一致:作为Schema.BigDecimal.check(...)的参数使用,例如:
import { BigDecimal, Schema } from "effect" // 校验金额严格大于 0 const PositiveAmount = Schema.BigDecimal.check( Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("0")) ) // 校验利率在 [0, 1] 区间 const Rate = Schema.BigDecimal.check( Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("0"), maximum: BigDecimal.fromStringUnsafe("1") }) )关于参数值的构造方式:
BigDecimal.fromStringUnsafe("123.45"):从十进制字符串构造,语义直观,是推荐写法;BigDecimal.make(123456n, 3):直接指定有效数字value与小数位scale(即 123.456),见 BigDecimal.ts。
用 check 组合出带约束的 Schema
Schema.BigDecimal本身仅校验值"是否为BigDecimal",不附带范围约束;比较校验器则通过Schema.bigDecimal.check(check)挂载过滤条件。check可以串联多个条件:
import { BigDecimal, Schema } from "effect" // 金额必须大于 0 且小于 1,000,000 const Amount = Schema.BigDecimal.check( Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("0")), Schema.isLessThanBigDecimal(BigDecimal.fromStringUnsafe("1000000")) )校验器的语义与参数边界(含/不含)在测试中有严格定义,见 Schema.test.ts:
isGreaterThanBigDecimal(1):2通过,1失败,错误消息Expected a value greater than 1;isGreaterThanOrEqualToBigDecimal(1):1通过,0失败,错误消息Expected a value greater than or equal to 1;isLessThanBigDecimal(1):0通过,1失败,错误消息Expected a value less than 1;isLessThanOrEqualToBigDecimal(1):1通过,2失败,错误消息Expected a value less than or equal to 1;isBetweenBigDecimal({ minimum: 1, maximum: 5 }):3通过,0失败,错误消息Expected a value between 1 and 5。
这些错误消息中的数值均由BigDecimal.format格式化输出——当scale绝对值达到 16 及以上时自动切换为科学计数法(见 BigDecimal.ts),因此即使边界值极小或极大,报错信息依然可读。
isBetweenBigDecimal 的区间边界配置
isBetweenBigDecimal接受配置对象,默认最小/最大值都是包含(inclusive)的,可通过exclusiveMinimum/exclusiveMaximum分别排除边界:
import { BigDecimal, Schema } from "effect" // (1, 5):严格开区间 const Open = Schema.BigDecimal.check( Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("1"), maximum: BigDecimal.fromStringUnsafe("5"), exclusiveMinimum: true, exclusiveMaximum: true }) ) // [1, 5):上界排除 const HalfOpen = Schema.BigDecimal.check( Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("1"), maximum: BigDecimal.fromStringUnsafe("5"), exclusiveMaximum: true }) )对应的底层实现在 Schema.ts:
const gte = options.exclusiveMinimum ? greaterThan : greaterThanOrEqualTo const lte = options.exclusiveMaximum ? lessThan : lessThanOrEqualTo return makeFilter<T>( (input) => gte(input, options.minimum) && lte(input, options.maximum), { expected: `a value between ${formatter(options.minimum)}${options.exclusiveMinimum ? " (excluded)" : ""} and ${ formatter(options.maximum) }${options.exclusiveMaximum ? " (excluded)" : ""}` // ... } )即:排除下界时改用严格大于,排除上界时改用严格小于,并同步在错误消息中追加(excluded)标注;错误消息Expected a value between 1 (excluded) and 5能精确说明区间形态。
底层实现:Order、makeFilter 与可复用工厂
五个校验器并非为BigDecimal单独手写逻辑,而是对通用工厂函数的特化实例化。四个单边界工厂分别位于 Schema.ts:
makeIsGreaterThan({ order, formatter }):返回(exclusiveMinimum, annotations?) => Filter,内部用Order.isGreaterThan(order)比较;makeIsGreaterThanOrEqualTo({ order, formatter }):返回(minimum, annotations?) => Filter,内部用Order.isGreaterThanOrEqualTo(order);makeIsLessThan({ order, formatter }):返回(exclusiveMaximum, annotations?) => Filter,内部用Order.isLessThan(order);makeIsLessThanOrEqualTo({ order, formatter }):返回(maximum, annotations?) => Filter,内部用Order.isLessThanOrEqualTo(order)。
所有工厂最终都通过makeFilter<T>(predicate, annotations)生成一个可挂载到 schema 上的 Filter,其 annotations 会携带expected消息与arbitrary.constraint.ordered(供测试数据生成使用),并可接收调用方传入的额外annotations。
BigDecimal五个校验器的具体定义(Schema.ts)统一传入:
export const isGreaterThanBigDecimal = makeIsGreaterThan({ order: BigDecimal_.Order, formatter: (bd) => BigDecimal_.format(bd) })这里有两个关键设计:
order: BigDecimal_.Order:比较顺序直接复用 BigDecimal.ts 中定义的Order实例。它先比较符号,再通过scale对齐后比较bigint有效数字,确保不同scale表示的同一数值(如 1.5 与 1.50)比较结果一致。formatter: (bd) => BigDecimal_.format(bd):将BigDecimal格式化为十进制字符串(必要时切换科学计数法),用于生成人类可读的expected错误消息,例如Expected a value greater than 1中的1。
正因这套工厂设计是泛化的,isGreaterThan/isGreaterThanOrEqualTo/isLessThan/isLessThanOrEqualTo/isBetween同样被复用于Number、Date、BigInt等有序类型(见 Schema.ts 附近的isBetween、Schema.ts 的isBetweenDate、Schema.ts 的isBetweenBigInt),BigDecimal版本只是换了一套order与formatter。
与 BigDecimal / BigDecimalFromString 的配合
Schema 模块还提供两个相关的数据类型 schema:
Schema.BigDecimal:校验值本身即为BigDecimal实例(通过BigDecimal.isBigDecimal判定,见 Schema.ts),编码时输出为字符串;Schema.BigDecimalFromString:将字符串解析为BigDecimal(解码用BigDecimal.fromString,编码用BigDecimal.format),见 Schema.ts。
在实际项目中,数据往往以 JSON 字符串(而非BigDecimal实例)进入系统。常见组合是先解析再校验:
import { BigDecimal, Schema } from "effect" // 先解析字符串,再校验区间 [1, 5] const ParsedBoundedAmount = Schema.BigDecimalFromString.pipe( Schema.filter( Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("1"), maximum: BigDecimal.fromStringUnsafe("5") }) ) )这样一次decode即可完成"字符串 → 任意精度小数 → 范围校验"的完整链路,同时BigDecimalFromString会在编码阶段把结果重新序列化为字符串,保证 JSON 传输格式一致。
任意值生成:属性测试中的边界处理
Schema的校验器还承担着为fast-check推导随机测试数据的职责(toArbitrary)。当Schema.BigDecimal.check(...)挂载了有序约束后,任意值生成会利用arbitrary.constraint.ordered中的order、minimum、maximum、exclusiveMinimum、exclusiveMaximum信息(见 Schema.ts),而不是简单地生成任意BigDecimal。
为支持这一过程,源码中实现了一组小数刻度(scale)处理辅助函数(Schema.ts):
- 默认最大刻度
bigDecimalDefaultMaxScale = 20; bigDecimalMaxScale:取默认刻度与最小/最大值刻度(排除边界时再加 1)中的最大值;bigDecimalValueConstraintsAtScale:把最小/最大值按刻度对齐为bigint约束,若最小大于最大则返回undefined;bigDecimalScaleConstraints:通过二分查找确定可生成的最小可行刻度区间;若无法生成任何满足条件的值(例如开区间内没有整数值),抛出错误Unable to derive an arbitrary for the ordered BigDecimal constraints。
这些辅助函数的存在,使得任意值生成可以覆盖大量边界场景,相关测试见 toArbitrary.test.ts:
- 带小数的区间(1.01 到 1.02);
- 同时排除上下界的开区间;
- 负小数的开区间(-1.02 到 -1.01);
- 最小等于最大但两端均包含的单点区间(此时仍可生成唯一值);
- 上界刻度远高于默认刻度(如 0 到 0.00000000000000000001);
- 不可能区间:
minimum === maximum且两端均排除(1.01, 1.01 开区间)时,toArbitrary抛出Unable to derive an arbitrary for the ordered BigDecimal constraints; - 组合校验
isGreaterThanBigDecimal(1.01)+isLessThanBigDecimal(1.01)同样不可生成任意值并抛错; - 通过
Order.flip(BigDecimal.Order)构造非自然顺序时,校验器依然能正确工作(toArbitrary.test.ts)。
这说明:即便不手写fast-check生成器,只要正确组合BigDecimal与比较校验器,属性测试也能在满足约束的空间内自动采样,同时对不可能满足的约束给出明确错误。
实战示例:金额与比例字段的完整校验
将上述能力组合起来,可以得到一个完整的业务模型示例:
import { BigDecimal, Schema } from "effect" // 收款金额:字符串输入,解析后必须大于 0 const Amount = Schema.BigDecimalFromString.pipe( Schema.filter(Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("0"))) ) // 折扣比例:解析后必须位于 (0, 1) const DiscountRate = Schema.BigDecimalFromString.pipe( Schema.filter( Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("0"), maximum: BigDecimal.fromStringUnsafe("1"), exclusiveMinimum: true, exclusiveMaximum: true }) ) ) // 订单模型:金额与折扣比例经过解析 + 范围校验 const Order = Schema.Struct({ id: Schema.String, amount: Amount, discountRate: DiscountRate }) // 校验失败时抛出包含精确错误消息的 ParseError Order.decode({ id: "o-1", amount: "-5", discountRate: "1.5" }) // => ParseError: amount: Expected a value greater than 0 // discountRate: Expected a value between 0 (excluded) and 1 (excluded)要点小结:
- 五个校验器分别覆盖
>、>=、<、<=、区间共五种比较语义,区间版本支持分别排除上下界; - 它们都基于
BigDecimal.Order与BigDecimal.format,因此比较精确、报错可读,且支持任意刻度(含科学计数法回退); - 底层由
makeIsGreaterThan等四个单边界工厂与makeIsBetween区间工厂特化而来,同一套机制也服务Number、Date、BigInt等类型; - 与
Schema.BigDecimalFromString配合可完成"字符串 → 任意精度小数 → 范围校验 → 字符串输出"的完整编解码链路; - 挂载到
check上后,fast-check任意值生成会自动遵循约束,并能在约束不可能满足时明确抛错。
如果想深入了解过滤条件(Filter)体系、Order比较器或BigDecimal的完整运算能力,可以继续阅读:
- Schema.ts 中
makeFilter、makeIsBetween等工厂与BigDecimal五个校验器的完整定义; - BigDecimal.ts 中
Order、format、make等基础构件; - Schema.test.ts 中五个校验器的解码行为测试;
- toArbitrary.test.ts 中任意值生成的边界场景测试。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考