news 2026/9/14 2:41:35

Effect Schema 日期时间与时区 Schema 全解析:DateTimeZoned、TimeZone、TimeZoneOffset 与 TimeZoneNamed 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Effect Schema 日期时间与时区 Schema 全解析:DateTimeZoned、TimeZone、TimeZoneOffset 与 TimeZoneNamed 实战指南

Effect Schema 日期时间与时区 Schema 全解析:DateTimeZoned、TimeZone、TimeZoneOffset 与 TimeZoneNamed 实战指南

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

在跨时区业务系统(如国际化 SaaS、日程调度、日志与审计、API 数据交换)中,日期时间的建模与序列化始终是绕不开的痛点:既要保留 IANA 命名的时区语义(如Europe/London),又要兼容简单的固定偏移(如+03:00),还要确保这些值能安全地穿越 JSON 边界。本篇文章基于 Effect 仓库中关于 Schema 新增DateTimeZonedTimeZoneOffsetTimeZoneNamedTimeZone四个 Schema 的变更记录(见 .changeset/pre/add-schema-datetime.md),结合 Schema.ts 与 DateTime.ts 的源码实现及 Schema.test.ts 中的测试用例,系统讲解这些 Schema 的用途、JSON 编解码行为、Arbitrary 生成规则与选型建议。读完本文,你将能熟练地为带时区的日期时间数据设计严谨的 Schema,并正确地在字符串、数字与DateTime值之间完成类型安全的转换。

变更背景:Schema 与 DateTime 模块的正式打通

该变更记录(changeset)声明:

Schema: addDateTimeZoned,TimeZoneOffset,TimeZoneNamed, andTimeZoneschemas.

也就是说,Effect 的 Schema 模块(effect/Schema)正式引入了对DateTime模块核心类型的 Schema 支持。在此之前,Schema 只能以通用String/Number处理日期时间,无法区分 UTC、命名时区、偏移时区与带时区的完整时刻。本次新增的四个 Schema 与既有的DateTimeUtc系列一起,构成了完整的日期时间 Schema 家族,其源码统一位于 Schema.ts 的 "DateTime schemas" 区块。

从版本标注看:

  • DateTimeUtcTimeZoneOffsetTimeZoneNamedTimeZoneDateTimeZoned3.10.0起提供(声明式 Schema,用于校验已存在的DateTime值);
  • DateTimeUtcFromStringDateTimeUtcFromMillisTimeZoneNamedFromStringTimeZoneFromStringDateTimeZonedFromString4.0.0起提供(转换式 Schema,用于从原始输入解析)。

在深入四个 Schema 之前,先明确DateTime模块中时区(TimeZone)的两种底层类型,这是理解一切的基础。

前置知识:DateTime 模块中的两种时区类型

DateTime.TimeZone是带判别联合类型,包含两个分支(源码见 DateTime.ts):

  • DateTime.TimeZone.Named:使用 IANA 时区标识(如"Europe/London""Asia/Tokyo")。它承载完整的 DST(夏令时)规则,能根据具体时刻正确换算。创建方式为DateTime.zoneMakeNamed/DateTime.zoneMakeNamedUnsafe
  • DateTime.TimeZone.Offset:固定偏移量,内部以毫秒表示(如3 * 60 * 60 * 1000表示 UTC+3)。创建方式为DateTime.zoneMakeOffset,其offset属性即毫秒数,可经DateTime.zoneToString序列化为"+03:00"形式。

这两个分支通过DateTime.isTimeZoneOffset/DateTime.isTimeZoneNamed判别函数区分——这正是后续各 Schema 声明式declare校验的依据。

Schema 逐一详解:四个新增 Schema 的建模与编解码

Schema.TimeZoneOffset:毫秒偏移量 ↔ 数字

TimeZoneOffsetdeclare声明式 Schema,类型层面对应DateTime.TimeZone.Offset,用于校验已经构造好的偏移时区值。其默认 JSON 序列化行为(源码 Schema.ts):

  • 编码:将DateTime.TimeZone.Offset编码为数字(毫秒偏移量)
  • 解码:将数字通过DateTime.zoneMakeOffset(n)还原为偏移时区值。

底层转换由timeZoneOffsetFromNumber完成(Schema.ts):decode调用DateTime.zoneMakeOffset(n)encode直接取tz.offsettoFormatter则使用DateTime.zoneToString输出人类可读的"+03:00"形式。

测试用例(Schema.test.ts)验证了DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)可被正常解码与编码。

import * as DateTime from "effect/DateTime" import * as Schema from "effect/Schema" const schema = Schema.TimeZoneOffset // 解码:直接传入已构造的偏移时区值 Schema.decodeSync(schema)(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // 编码:得到毫秒偏移量数字 Schema.encodeSync(schema)(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // => 10800000

Schema.TimeZoneNamed:IANA 命名时区 ↔ 字符串

TimeZoneNamed同样为声明式 Schema,校验DateTime.TimeZone.Named值(Schema.ts)。默认 JSON 序列化行为:

  • 编码:输出 IANA 时区标识字符串(如"Europe/London");
  • 解码:要求输入已是DateTime.TimeZone.Named值。

其转换底层为timeZoneNamedFromString(Schema.ts):encode返回tz.iddecode调用DateTime.zoneMakeNamed(s),若无法识别则产生"a valid IANA time zone"InvalidValue失败(对应测试 Schema.test.ts 中Europe/London的成功往返)。

值得注意的差异:TimeZoneNamed的默认 JSON 编解码并不接受任意字符串解析(它只校验已构造的 Named 值,字符串解析由后面的TimeZoneNamedFromString负责),但其Arbitrary 生成能力十分明确——从arbitraryNamedTimeZones常量("UTC""Europe/London""America/New_York""Asia/Tokyo""Australia/Sydney")中随机选取,再经DateTime.zoneMakeNamedUnsafe构造,见 Schema.ts 与 Schema.ts。

Schema.TimeZone:时区联合类型的"大统一"Schema

TimeZone同时覆盖 Named 与 Offset 两种分支,是对DateTime.TimeZone整个类型的声明式 Schema(Schema.ts)。默认 JSON 序列化行为:

  • 编码:统一输出字符串——命名时区输出 IANA 标识("Europe/London"),偏移时区输出"+03:00"风格;
  • 解码:要求输入已是DateTime.TimeZone值(Named 或 Offset 均可)。

底层转换timeZoneFromString(Schema.ts)的decode使用DateTime.zoneFromStringencode使用DateTime.zoneToString。测试(Schema.test.ts)验证了偏移时区与命名时区都能成功往返。

TimeZone的 Arbitrary 生成策略最有代表性(Schema.ts):它构造一个Union——dateTimeArbitraryInteger(-12h, +14h)(毫秒偏移数字)与Literals(arbitraryNamedTimeZones)(五个 IANA 标识)的联合,随后decode时根据值的类型(数字 →zoneMakeOffset,字符串 →zoneMakeNamedUnsafe)分别构造对应分支。

import * as Schema from "effect/Schema" const schema = Schema.TimeZone // 编码:命名时区 → IANA 字符串 Schema.encodeSync(schema)(DateTime.zoneMakeNamedUnsafe("Europe/London")) // => "Europe/London" // 编码:偏移时区 → "+03:00" 字符串 Schema.encodeSync(schema)(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // => "+03:00"

Schema.DateTimeZoned:保留时区语义的完整时刻

DateTimeZoned是对DateTime.Zoned(时刻 + 时区)的声明式 Schema,是最具实战价值的一个(Schema.ts)。它的默认 JSON 序列化行为定义了两套格式:

  • 偏移时区:编码为带数字偏移的 ISO 日期时间,如YYYY-MM-DDTHH:mm:ss.sss+HH:MM
  • 命名时区:编码为 ISO 日期时间 + 方括号包裹的 IANA 标识,如2024-01-01T00:00:00.000+00:00[Europe/London](即DateTime.formatIsoZoned的输出,见 DateTime.ts)。

底层转换dateTimeZonedFromString(Schema.ts)的decode使用DateTime.makeZonedFromStringencode使用DateTime.formatIsoZoned。注意DateTimeZoned本身只校验已构造的DateTime.Zoned;对字符串的解析由 4.0.0 的DateTimeZonedFromString承担。

DateTimeZoned的 Arbitrary 生成逻辑(Schema.ts)非常精细:

  • 时刻通过dateTimeArbitraryBounds约束在±8640000000000000 ms并内缩14harbitraryMinimumZonedDateTimeTimestamp/arbitraryMaximumZonedDateTimeTimestamp),以容纳任意时区偏移后仍在合法日期范围内;
  • 时区复用timeZoneArbitrarySchema()(偏移数字与 IANA 标识的 Union);
  • 最终以{ epochMilliseconds, timeZone }结构经DateTime.makeZonedUnsafe构造出DateTime.Zoned

同时DateTimeZoned还通过toEquivalence: () => DateTime.Equivalence提供了基于时刻的等价性判断(Schema.ts)。

import * as DateTime from "effect/DateTime" import * as Schema from "effect/Schema" const schema = Schema.DateTimeZoned const zoned = DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" }) // 编码:命名时区 → ISO + [IANA] Schema.encodeSync(schema)(zoned) // => "2021-01-01T00:00:00.000+00:00[Europe/London]"

4.0.0 起可用的 FromString 系列:从原始输入安全解析

上述四个声明式 Schema 只负责校验"已经是 DateTime 值"的数据;要从字符串/数字原始输入解析出DateTime值,需要 4.0.0 引入的转换式变体,它们均基于decodeTo实现:

Schema输入输出底层解析函数
TimeZoneNamedFromStringstring(IANA 标识)DateTime.TimeZone.NamedDateTime.zoneMakeNamed
TimeZoneFromStringstring(IANA 标识或+03:00DateTime.TimeZoneDateTime.zoneFromString
DateTimeZonedFromStringstring...Z[Europe/London]格式)DateTime.ZonedDateTime.makeZonedFromString
DateTimeUtcFromStringstring(可被DateTime.make接受的日期时间串)DateTime.UtcDateTime.make+toUtc
DateTimeUtcFromMillisnumber(epoch 毫秒)DateTime.UtcDateTime.make+toUtc

测试用例覆盖了这些转换式 Schema 的失败路径,例如(Schema.test.ts):

const schema = Schema.DateTimeZonedFromString // 编码:DateTime.Zoned → ISO + [IANA] 字符串 Schema.encodeSync(schema)(zoned) // => DateTime.formatIsoZoned(zoned) // 解码失败:非法字符串 Schema.decodeUnknownSync(schema)("invalid") // => ParseError: Expected a valid Zoned DateTime string

再如TimeZoneFromString可同时解析"Europe/London""+03:00",而TimeZoneNamedFromString只接受 IANA 标识(Schema.test.ts):

const schema = Schema.TimeZoneFromString Schema.decodeUnknownSync(schema)("Europe/London") // => zoneMakeNamedUnsafe("Europe/London") Schema.decodeUnknownSync(schema)("+03:00") // => zoneMakeOffset(3 * 60 * 60 * 1000)

这些转换式 Schema 内部均采用transformEffectSchemaTransformation.transformEffect),解析失败时通过SchemaIssue.InvalidValue携带明确的期望信息(如"a valid Zoned DateTime string""a valid IANA time zone""a valid time zone"),可无缝融入 Effect 的管道式错误处理流程。

实战选型指南:何时用哪个 Schema?

综合源码中的版本划分与语义,给出如下选型建议:

  1. 持久化 / API 传输带时区的完整时刻→ 首选DateTimeZoned(或解析字符串输入用DateTimeZonedFromString)。JSON 格式...+00:00[Europe/London]同时保留时刻与 DST 语义,比单纯偏移更可靠。
  2. 只需要 UTC 时刻→ 使用DateTimeUtc/DateTimeUtcFromString/DateTimeUtcFromMillis,输出无歧义的 UTC ISO 字符串或 epoch 毫秒,体积最小。
  3. 单独建模时区字段(如"用户偏好时区"列)→ 用TimeZoneNamed(限制为 IANA 命名时区)或TimeZone(允许偏移与命名并存,如"+08:00""Asia/Tokyo")。
  4. 需要任意值校验、不需要解析→ 用四个声明式 Schema(TimeZoneOffset/TimeZoneNamed/TimeZone/DateTimeZoned),配合Schema.decodeSync使用;同时它们各自提供 JSON 序列化行为,可用于encode
  5. 属性校验与测试数据生成→ 依赖 Arbitrary 生成:TimeZoneNamedTimeZone从五个常见 IANA 区域生成,DateTimeZoned在全日期域内联合同步生成时刻与时区,可无缝接入 effect/Schema 的 FastCheck 属性测试 流程。

在组合场景中,这些 Schema 可直接嵌入Schema.Struct,例如:

const UserPreferences = Schema.Struct({ createdAt: Schema.DateTimeZoned, // 完整带时区时刻 locale: Schema.String, timeZone: Schema.TimeZoneNamed // IANA 命名时区 })

小结

本次 changeset 将DateTime模块的时区与带时区时刻类型正式纳入 Schema 体系,补齐了日期时间建模的关键一环。四个核心 Schema 各司其职:TimeZoneOffset以毫秒数字承载固定偏移、TimeZoneNamed以 IANA 字符串承载命名时区、TimeZone统一覆盖两种分支、DateTimeZoned则完整保留"时刻 + 时区"语义并以 ISO +[Zone]格式穿越 JSON 边界。配合 4.0.0 的*FromString系列与各 Schema 内置的 Arbitrary 生成规则,开发者可以放心地在 HTTP API、数据库持久化与属性测试场景中构建类型安全、时区正确的日期时间管线。相关实现与测试可继续阅读 Schema.ts 与 Schema.test.ts 获取一手依据。

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:39:39

如何用 marimo islands 把交互式笔记本内容嵌入静态网页?

如何用 marimo islands 把交互式笔记本内容嵌入静态网页? 【免费下载链接】marimo A reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All …

作者头像 李华
网站建设 2026/9/14 2:36:36

工业级可燃气体变送器原理与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 2:34:33

把小爱音箱接入 ChatGPT:MiGPT 语音助手 10 分钟部署教程

把小爱音箱接入 ChatGPT:MiGPT 语音助手 10 分钟部署教程 【免费下载链接】mi-gpt 🏠 将小爱音箱接入 ChatGPT 和豆包,改造成你的专属语音助手。 项目地址: https://gitcode.com/GitHub_Trending/mi/mi-gpt MiGPT 是一个把小米小爱音箱…

作者头像 李华
网站建设 2026/9/14 2:33:43

Obsidian多端同步难题破解:五大方案实测与选型指南

我在Obsidian上折腾同步已经有8年了,从最早的移动硬盘手动拷贝,到后来的各种插件、网盘、Git仓库,几乎把市面上能用的方案都试了一遍。写这篇东西的起因很简单:前几天帮我朋友从Notion迁到Obsidian,第一句话就问“多端…

作者头像 李华