DateTime
使用 Effect 的 DateTime 处理精确的时间点,支持创建、比较和算术运算,从而高效地处理时间。
在 JavaScript 中处理日期和时间可能颇费周折。内置的 Date 对象会修改自身的内部状态,时区处理也可能令人困惑。这些设计选择会在开发依赖日期时间准确性的应用时引入错误,例如调度系统、时间戳服务或日志工具。
DateTime 模块旨在通过提供以下特性来解决这些局限:
- 不可变数据:每个
DateTime都是不可变结构,可减少与就地修改相关的错误。 - 时区支持:
DateTime为时区提供了完善的支持,包括自动处理夏令时调整。 - 算术运算:你可以对
DateTime实例执行算术运算,例如加上或减去一个时长(duration)。
DateTime 类型
DateTime 表示时间中的一个时刻。它既可以存储为简单的 UTC 值,也可以存储为带有关联时区的值。以这种方式存储时间,有助于你同时管理精确的时间戳,以及该时间应如何显示或解释的上下文。
DateTime 有两种主要变体:
-
Utc:一种不可变结构,使用
epochMilliseconds(自 Unix 纪元以来的毫秒数)表示协调世界时(UTC)中的一个时间点。 -
Zoned:包含
epochMilliseconds以及一个TimeZone,让你可以为时间戳附加偏移量或命名区域(如 “America/New_York”)。
为什么有两种变体?
- 如果你只需要一个通用参照,而不依赖本地时区,Utc 就很直接。
- 当你需要跟踪时区信息时,Zoned 会很有帮助,例如转换为本地时间或针对夏令时进行调整。
TimeZone 变体
TimeZone 可以是以下两种之一:
- Offset:表示相对 UTC 的固定偏移量(例如 UTC+2 或 UTC-5)。
- Named:使用命名区域(如 “Europe/London” 或 “America/New_York”),它会自动考虑特定区域的规则,例如夏令时变更。
TypeScript 定义
下面是 DateTime 类型的 TypeScript 定义:
type DateTime = Utc | Zoned
interface Utc {
readonly _tag: "Utc"
readonly epochMilliseconds: number
}
interface Zoned {
readonly _tag: "Zoned"
readonly epochMilliseconds: number
readonly zone: TimeZone
}
type TimeZone = TimeZone.Offset | TimeZone.Named
declare namespace TimeZone {
interface Offset {
readonly _tag: "Offset"
readonly offset: number
}
interface Named {
readonly _tag: "Named"
readonly id: string
}
}
DateTime.Parts 类型
DateTime.Parts 类型定义了日期的主要组成部分,例如年、月、日、小时、分钟和秒。
namespace DateTime {
interface Parts {
readonly millisecond: number
readonly second: number
readonly minute: number
readonly hour: number
readonly day: number
readonly month: number
readonly year: number
}
interface PartsWithWeekday extends Parts {
readonly weekDay: number
}
}
DateTime.Input 类型
DateTime.Input 是一种灵活的输入类型,可用于创建 DateTime 实例。它可以是以下之一:
- 一个
DateTime实例 - 一个 JavaScript
Date对象 - 一个表示自 Unix 纪元以来毫秒数的数值
- 一个包含部分日期 parts 的对象(例如
{ year: 2024, month: 1, day: 1 }) - 一个可由 JavaScript 的 Date.parse 解析的字符串
namespace DateTime {
type Input = DateTime | Partial<Parts> | Date | number | string
}
Utc 构造器
Utc 是一种不可变结构,使用 epochMilliseconds(自 Unix 纪元以来的毫秒数)表示协调世界时(UTC)中的一个时间点。
unsafeFromDate
从 JavaScript Date 创建一个 Utc。
如果提供的 Date 无效,则抛出 IllegalArgumentError。
传入 Date 对象时,它会被转换为 Utc 实例。该时间会被解释为执行代码的系统的本地时间,然后再调整为 UTC。这样可以确保日期和时间具有一致的、与时区无关的表示。
示例(把意大利的本地时间转换为 UTC)
下面的示例假设代码运行在位于意大利的系统上(CET 时区):
import { DateTime } from "effect"
// Create a Utc instance from a local JavaScript Date
//
// ┌─── Utc
// ▼
const utc = DateTime.fromDateUnsafe(new Date("2025-01-01 04:00:00"))
console.log(utc)
utc // => DateTime.makeUnsafe(1735700400000)
console.log(utc.epochMilliseconds)
utc.epochMilliseconds // => 1735700400000
解释:
- 本地时间 2025-01-01 04:00:00(意大利,CET)通过减去时区偏移量(1 月份为 UTC+1)转换为 UTC。
- 因此,UTC 时间变为 2025-01-01 03:00:00.000Z。
epochMilliseconds以自 Unix 纪元以来的毫秒数提供同一时间,确保 UTC 时间戳具有精确的数值表示。
unsafeMake
从 DateTime.Input 创建一个 Utc。
示例(使用 unsafeMake 创建 DateTime)
下面的示例假设代码运行在位于意大利的系统上(CET 时区):
import { DateTime } from "effect"
// From a JavaScript Date
const utc1 = DateTime.makeUnsafe(new Date("2025-01-01 04:00:00"))
console.log(utc1)
utc1 // => DateTime.makeUnsafe(1735700400000)
// From partial date parts
const utc2 = DateTime.makeUnsafe({ year: 2025 })
console.log(utc2)
utc2 // => DateTime.makeUnsafe(1735689600000)
// From a string
const utc3 = DateTime.makeUnsafe("2025-01-01")
console.log(utc3)
utc3 // => DateTime.makeUnsafe(1735689600000)
解释:
- 本地时间 2025-01-01 04:00:00(意大利,CET)通过减去时区偏移量(1 月份为 UTC+1)转换为 UTC。
- 因此,UTC 时间变为 2025-01-01 03:00:00.000Z。
make
与 unsafeMake 类似,但会在输入无效时返回一个 Option,而不是抛出错误。
如果输入无效,它返回 None;如果有效,则返回包含 Utc 的 Some。
示例(安全地创建 DateTime)
下面的示例假设代码运行在位于意大利的系统上(CET 时区):
import { DateTime, Option } from "effect"
// From a JavaScript Date
const maybeUtc1 = DateTime.make(new Date("2025-01-01 04:00:00"))
console.log(maybeUtc1)
maybeUtc1 // => Option.some(DateTime.makeUnsafe(1735700400000))
// From partial date parts
const maybeUtc2 = DateTime.make({ year: 2025 })
console.log(maybeUtc2)
maybeUtc2 // => Option.some(DateTime.makeUnsafe(1735689600000))
// From a string
const maybeUtc3 = DateTime.make("2025-01-01")
console.log(maybeUtc3)
maybeUtc3 // => Option.some(DateTime.makeUnsafe(1735689600000))
解释:
- 本地时间 2025-01-01 04:00:00(意大利,CET)通过减去时区偏移量(1 月份为 UTC+1)转换为 UTC。
- 因此,UTC 时间变为 2025-01-01 03:00:00.000Z。
Zoned 构造器
Zoned 包含 epochMilliseconds 以及一个 TimeZone,让你可以为时间戳附加偏移量或命名区域(如 “America/New_York”)。
unsafeMakeZoned
通过把 DateTime.Input 与可选的 TimeZone 组合来创建 Zoned。
这让你可以表示一个带有相关联时区的特定时间点。
时区可以通过几种方式提供:
- 作为一个
TimeZone对象 - 一个字符串标识符(例如
"Europe/London") - 一个以毫秒为单位的数值偏移量
如果输入或时区无效,则会抛出 IllegalArgumentError。
示例(创建未指定时区的 Zoned DateTime)
下面的示例假设代码运行在位于意大利的系统上(CET 时区):
import { DateTime } from "effect"
// Create a Zoned DateTime based on the system's local time zone
const zoned = DateTime.makeZonedUnsafe(new Date("2025-01-01 04:00:00"))
console.log(zoned)
zoned // => DateTime.makeZonedUnsafe(1735700400000, { timeZone: 3600000 })
console.log(zoned.zone)
zoned.zone // => DateTime.zoneMakeOffset(3600000)
这里使用系统的时区(CET,1 月份为 UTC+1)来创建 Zoned 实例。
示例(指定命名时区)
下面的示例假设代码运行在位于意大利的系统上(CET 时区):
import { DateTime } from "effect"
// Create a Zoned DateTime with a specified named time zone
const zoned = DateTime.makeZonedUnsafe(new Date("2025-01-01 04:00:00"), {
timeZone: "Europe/Rome",
})
console.log(zoned)
zoned // => DateTime.makeZonedUnsafe(1735700400000, { timeZone: "Europe/Rome" })
console.log(zoned.zone)
zoned.zone // => DateTime.zoneMakeNamedUnsafe("Europe/Rome")
在这个例子中,显式提供了 "Europe/Rome" 时区,因此 Zoned 实例会绑定到这个命名时区。
默认情况下,输入日期会被当作 UTC 值,然后针对指定的时区进行调整。若要把输入日期解释为处于指定时区中,可以使用 adjustForTimeZone 选项。
示例(按指定时区解释输入日期)
下面的示例假设代码运行在位于意大利的系统上(CET 时区):
import { DateTime } from "effect"
// Interpret the input date as being in the specified time zone
const zoned = DateTime.makeZonedUnsafe(new Date("2025-01-01 04:00:00"), {
timeZone: "Europe/Rome",
adjustForTimeZone: true,
})
console.log(zoned)
zoned // => DateTime.makeZonedUnsafe(1735696800000, { timeZone: "Europe/Rome" })
console.log(zoned.zone)
zoned.zone // => DateTime.zoneMakeNamedUnsafe("Europe/Rome")
解释
- 不使用
adjustForTimeZone:输入日期被解释为 UTC,然后调整为指定时区。例如,UTC 中的2025-01-01 04:00:00在 CET(UTC+1)中变为2025-01-01T04:00:00.000+01:00。 - 使用
adjustForTimeZone: true:输入日期被解释为处于指定时区中。例如,“Europe/Rome”(CET)中的2025-01-01 04:00:00会被调整为其对应的 UTC 时间,结果为2025-01-01T03:00:00.000+01:00。
makeZoned
makeZoned 函数的工作方式与 unsafeMakeZoned 类似,但提供了更安全的方式。当输入无效时,它不会抛出错误,而是返回一个 Option<Zoned>。
如果输入无效,它返回 None;如果有效,则返回包含 Zoned 的 Some。
示例(安全地创建 Zoned DateTime)
import { DateTime, Option } from "effect"
// ┌─── Option<Zoned>
// ▼
const zoned = DateTime.makeZoned(new Date("2025-01-01 04:00:00"), {
timeZone: "Europe/Rome",
})
if (Option.isSome(zoned)) {
console.log("The DateTime is valid")
}
Option.isSome(zoned) // => true
makeZonedFromString
通过解析格式为 YYYY-MM-DDTHH:mm:ss.sss+HH:MM[IANA timezone identifier] 的字符串来创建 Zoned。
如果输入字符串有效,函数返回包含 Zoned 的 Some;如果输入无效,则返回 None。
示例(从字符串解析 Zoned DateTime)
import { DateTime, Option } from "effect"
// ┌─── Option<Zoned>
// ▼
const zoned = DateTime.makeZonedFromString(
"2025-01-01T03:00:00.000+01:00[Europe/Rome]",
)
if (Option.isSome(zoned)) {
console.log("The DateTime is valid")
}
Option.isSome(zoned) // => true
当前时间
now
通过 Clock 服务,以 Effect<Utc> 的形式提供当前 UTC 时间。
示例(获取当前 UTC 时间)
import { DateTime, Effect } from "effect"
const program = Effect.gen(function* () {
// ┌─── Utc
// ▼
const currentTime = yield* DateTime.now
return DateTime.isUtc(currentTime)
})
await Effect.runPromise(program) // => true
使用 Clock 服务能确保时间在整个应用中保持一致,这在测试环境中尤其有用 ——
此时你可能需要控制或模拟当前时间。
unsafeNow
使用 Date.now() 立即获取当前 UTC 时间,不经过 Clock 服务。
示例(立即获取当前 UTC 时间)
import { DateTime } from "effect"
// ┌─── Utc
// ▼
const currentTime = DateTime.nowUnsafe()
DateTime.isUtc(currentTime) // => true
类型守卫
| 函数 | 说明 |
|---|---|
isDateTime | 检查一个值是否为 DateTime。 |
isTimeZone | 检查一个值是否为 TimeZone。 |
isTimeZoneOffset | 检查一个值是否为 TimeZone.Offset。 |
isTimeZoneNamed | 检查一个值是否为 TimeZone.Named。 |
isUtc | 检查一个 DateTime 是否为 Utc 变体。 |
isZoned | 检查一个 DateTime 是否为 Zoned 变体。 |
示例(校验一个 DateTime)
import { DateTime } from "effect"
function printDateTimeInfo(x: unknown) {
if (DateTime.isDateTime(x)) {
console.log("This is a valid DateTime")
} else {
console.log("Not a DateTime")
}
}
DateTime.isDateTime(DateTime.nowUnsafe()) // => true
DateTime.isDateTime("not a date") // => false
时区管理
| 函数 | 说明 |
|---|---|
setZone | 通过应用给定的 TimeZone,从 DateTime 创建 Zoned。 |
setZoneOffset | 使用固定偏移量(毫秒),从 DateTime 创建 Zoned。 |
setZoneNamed | 根据 IANA 时区标识符从 DateTime 创建 Zoned;若标识符无效则返回 None。 |
setZoneNamedUnsafe | 根据 IANA 时区标识符从 DateTime 创建 Zoned;若标识符无效则抛出异常。 |
zoneMakeNamedUnsafe | 根据 IANA 时区标识符创建 TimeZone.Named;若标识符无效则抛出异常。 |
zoneMakeNamed | 根据 IANA 时区标识符创建 TimeZone.Named;若标识符无效则返回 None。 |
zoneMakeNamedEffect | 根据 IANA 时区标识符创建 Effect<TimeZone.Named, IllegalArgumentError>;若标识符无效则以 IllegalArgumentError 失败 |
zoneMakeOffset | 根据以毫秒为单位的数值偏移量创建 TimeZone.Offset。 |
zoneMakeLocal | 根据系统的本地时区创建 TimeZone.Named。 |
zoneFromString | 尝试从字符串解析时区;若无效则返回 None。 |
zoneToString | 返回 TimeZone 的字符串表示形式。 |
示例(把时区应用到 DateTime)
import { DateTime } from "effect"
// Create a UTC DateTime
//
// ┌─── Utc
// ▼
const utc = DateTime.makeUnsafe("2024-01-01")
// Create a named time zone for New York
//
// ┌─── TimeZone.Named
// ▼
const zoneNY = DateTime.zoneMakeNamedUnsafe("America/New_York")
// Apply it to the DateTime
//
// ┌─── Zoned
// ▼
const zoned = DateTime.setZone(utc, zoneNY)
console.log(zoned)
zoned // => DateTime.makeZonedUnsafe(1704067200000, { timeZone: "America/New_York" })
zoneFromString
解析字符串以创建 DateTime.TimeZone。
该函数会尝试把输入的字符串解释为以下两种形式之一:
- 数值形式的时区偏移量(例如 “GMT”、“+01:00”)
- IANA 时区标识符(例如 “Europe/London”)
如果字符串匹配偏移量格式,就会转换为 TimeZone.Offset。
否则,它会尝试用该输入创建一个 TimeZone.Named。
如果输入的字符串无效,则返回 Option.none()。
示例(从字符串解析时区)
import { DateTime, Option } from "effect"
// Attempt to parse a numeric offset
const offsetZone = DateTime.zoneFromString("+01:00")
console.log(Option.isSome(offsetZone))
Option.isSome(offsetZone) // => true
// Attempt to parse an IANA time zone
const namedZone = DateTime.zoneFromString("Europe/London")
console.log(Option.isSome(namedZone))
Option.isSome(namedZone) // => true
// Invalid input
const invalidZone = DateTime.zoneFromString("Invalid/Zone")
console.log(Option.isSome(invalidZone))
Option.isSome(invalidZone) // => false
比较
| 函数 | 说明 |
|---|---|
distance | 返回两个 DateTime 之间的差值,以带符号的 Duration 表示(若 other 早于 self 则为负)。 |
min | 返回两个 DateTime 值中较早的那个。 |
max | 返回两个 DateTime 值中较晚的那个。 |
isGreaterThan、isGreaterThanOrEqualTo 等 | 检查两个 DateTime 值之间的先后顺序。 |
between | 检查某个 DateTime 是否落在给定的边界范围内。 |
isFuture、isPast、isFutureUnsafe 等 | 检查某个 DateTime 位于未来还是过去。 |
示例(求两个 DateTime 之间的距离)
import { DateTime, Duration } from "effect"
const utc1 = DateTime.makeUnsafe("2025-01-01T00:00:00Z")
const utc2 = DateTime.add(utc1, { days: 1 })
// `distance` returns a signed Duration directly (one day)
console.log(DateTime.distance(utc1, utc2))
DateTime.distance(utc1, utc2) // => Duration.millis(86400000)
转换
| 函数 | 说明 |
|---|---|
toDateUtc | 返回 UTC 下的 JavaScript Date。 |
toDate | 应用时区(如果存在),并转换为 JavaScript Date。 |
zonedOffset | 对于 Zoned 类型的 DateTime,返回以毫秒为单位的时区偏移量。 |
zonedOffsetIso | 对于 Zoned 类型的 DateTime,返回形如 “+01:00” 的 ISO 偏移量字符串。 |
toEpochMillis | 返回以毫秒为单位的 Unix 纪元时间。 |
removeTime | 返回一个清除了时间部分的 Utc(只保留日期)。 |
日期各部分
| 函数 | 说明 |
|---|---|
toParts | 返回按调整时区后的日期各部分(包括星期几)。 |
toPartsUtc | 返回 UTC 下的日期各部分(包括星期几)。 |
getPart / getPartUtc | 从日期中取出指定的部分(例如 "year" 或 "month")。 |
setParts / setPartsUtc | 更新日期的某些部分,同时保留或忽略时区。 |
示例(从 DateTime 中提取各部分)
import { DateTime } from "effect"
const zoned = DateTime.setZone(
DateTime.makeUnsafe("2024-01-01"),
DateTime.zoneMakeNamedUnsafe("Europe/Rome"),
)
console.log(DateTime.getPart(zoned, "month"))
DateTime.getPart(zoned, "month") // => 1
运算
| 函数 | 说明 |
|---|---|
addDuration | 把给定的 Duration 加到 DateTime 上。 |
subtractDuration | 从 DateTime 中减去给定的 Duration。 |
add | 把数值形式的各部分(例如 { hours: 2 })加到 DateTime 上。 |
subtract | 减去数值形式的各部分。 |
startOf | 把 DateTime 移动到给定单位的起点(例如一天的开始或一个月的开始)。 |
endOf | 把 DateTime 移动到给定单位的终点。 |
nearest | 把 DateTime 舍入到最近的指定单位。 |
格式化
| 函数 | 说明 |
|---|---|
format | 使用 DateTimeFormat API 把 DateTime 格式化为字符串。 |
formatLocal | 使用系统本地时区和区域设置进行格式化。 |
formatUtc | 强制按 UTC 格式化。 |
formatIntl | 使用传入的 Intl.DateTimeFormat。 |
formatIso | 返回 UTC 下的 ISO 8601 字符串。 |
formatIsoDate | 返回经过时区调整的 ISO 日期字符串。 |
formatIsoDateUtc | 返回 UTC 下的 ISO 日期字符串。 |
formatIsoOffset | 把 Zoned 格式化为带偏移量(形如 “+01:00”)的字符串。 |
formatIsoZoned | 按 YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Zone] 的形式格式化 Zoned。 |
用于当前时区的 Layer
| 函数 | 说明 |
|---|---|
CurrentTimeZone | 当前时区对应的服务键。 |
setZoneCurrent | 让某个 DateTime 使用当前时区。 |
withCurrentZone | 为某个 effect 提供指定的时区。 |
withCurrentZoneLocal | 为该 effect 使用系统本地时区。 |
withCurrentZoneOffset | 为该 effect 使用固定偏移量(毫秒)。 |
withCurrentZoneNamed | 使用具名时区标识符(例如 “Europe/London”)。 |
nowInCurrentZone | 以配置的时区获取当前时间,结果为 Zoned。 |
layerCurrentZone | 创建一个提供 CurrentTimeZone 服务的 Layer。 |
layerCurrentZoneOffset | 根据固定偏移量创建 Layer。 |
layerCurrentZoneNamed | 根据具名时区创建 Layer,若无效则失败。 |
layerCurrentZoneLocal | 根据系统本地时区创建 Layer。 |
示例(在 Effect 中使用当前时区)
import { DateTime, Effect } from "effect"
// Retrieve the current time in the "Europe/London" time zone
const program = Effect.gen(function* () {
const zonedNow = yield* DateTime.nowInCurrentZone
console.log(zonedNow)
return zonedNow
}).pipe(DateTime.withCurrentZoneNamed("Europe/London"))
const zonedNow = await Effect.runPromise(program)
/*
Example Output:
DateTime.Zoned(2025-01-06T18:36:38.573+00:00[Europe/London])
*/
zonedNow.zone // => DateTime.zoneMakeNamedUnsafe("Europe/London")