Effect Schema 的 BigDecimal 比较校验:isGreaterThanBigDecimal 等五个高阶校验器详解
2026/9/13 21:07:59 网站建设 项目流程

Effect Schema 的 BigDecimal 比较校验:isGreaterThanBigDecimal 等五个高阶校验器详解

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

本文基于effect包 4.0.0 的 Schema 模块,围绕其新增的BigDecimal比较校验 API(isGreaterThanBigDecimalisGreaterThanOrEqualToBigDecimalisLessThanBigDecimalisLessThanOrEqualToBigDecimalisBetweenBigDecimal)展开。文章覆盖这五个校验器的语义、参数与边界行为,并结合源码解析其基于OrdermakeFilter的底层实现、对fast-check任意值生成的约束推导,以及相应的测试用例,帮助读者在需要精确小数范围约束的场景(金额、利率、计量等)中正确使用 Effect Schema 完成运行时校验。

为什么需要 BigDecimal 校验器

JavaScript 的Number基于 IEEE 754 双精度浮点数,无法精确表示所有十进制小数,而金融、计量、科学计算等领域要求数值比较不丢失精度。Effect 为此提供了任意精度十进制类型BigDecimal(以value: bigintscale: 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) })

这里有两个关键设计:

  1. order: BigDecimal_.Order:比较顺序直接复用 BigDecimal.ts 中定义的Order实例。它先比较符号,再通过scale对齐后比较bigint有效数字,确保不同scale表示的同一数值(如 1.5 与 1.50)比较结果一致。
  2. formatter: (bd) => BigDecimal_.format(bd):将BigDecimal格式化为十进制字符串(必要时切换科学计数法),用于生成人类可读的expected错误消息,例如Expected a value greater than 1中的1

正因这套工厂设计是泛化的,isGreaterThan/isGreaterThanOrEqualTo/isLessThan/isLessThanOrEqualTo/isBetween同样被复用于NumberDateBigInt等有序类型(见 Schema.ts 附近的isBetween、Schema.ts 的isBetweenDate、Schema.ts 的isBetweenBigInt),BigDecimal版本只是换了一套orderformatter

与 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中的orderminimummaximumexclusiveMinimumexclusiveMaximum信息(见 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)

要点小结:

  1. 五个校验器分别覆盖>>=<<=、区间共五种比较语义,区间版本支持分别排除上下界;
  2. 它们都基于BigDecimal.OrderBigDecimal.format,因此比较精确、报错可读,且支持任意刻度(含科学计数法回退);
  3. 底层由makeIsGreaterThan等四个单边界工厂与makeIsBetween区间工厂特化而来,同一套机制也服务NumberDateBigInt等类型;
  4. Schema.BigDecimalFromString配合可完成"字符串 → 任意精度小数 → 范围校验 → 字符串输出"的完整编解码链路;
  5. 挂载到check上后,fast-check任意值生成会自动遵循约束,并能在约束不可能满足时明确抛错。

如果想深入了解过滤条件(Filter)体系、Order比较器或BigDecimal的完整运算能力,可以继续阅读:

  • Schema.ts 中makeFiltermakeIsBetween等工厂与BigDecimal五个校验器的完整定义;
  • BigDecimal.ts 中Orderformatmake等基础构件;
  • Schema.test.ts 中五个校验器的解码行为测试;
  • toArbitrary.test.ts 中任意值生成的边界场景测试。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询