已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

DateTime

使用 Effect 的 DateTime 处理精确的时间点,支持创建、比较和算术运算,从而高效地处理时间。

在 JavaScript 中处理日期和时间可能颇费周折。内置的 Date 对象会修改自身的内部状态,时区处理也可能令人困惑。这些设计选择会在开发依赖日期时间准确性的应用时引入错误,例如调度系统、时间戳服务或日志工具。

DateTime 模块旨在通过提供以下特性来解决这些局限:

  • 不可变数据:每个 DateTime 都是不可变结构,可减少与就地修改相关的错误。
  • 时区支持DateTime 为时区提供了完善的支持,包括自动处理夏令时调整。
  • 算术运算:你可以对 DateTime 实例执行算术运算,例如加上或减去一个时长(duration)。

DateTime 类型

DateTime 表示时间中的一个时刻。它既可以存储为简单的 UTC 值,也可以存储为带有关联时区的值。以这种方式存储时间,有助于你同时管理精确的时间戳,以及该时间应如何显示或解释的上下文。

DateTime 有两种主要变体:

  1. Utc:一种不可变结构,使用 epochMilliseconds(自 Unix 纪元以来的毫秒数)表示协调世界时(UTC)中的一个时间点。

  2. 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;如果有效,则返回包含 UtcSome

示例(安全地创建 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;如果有效,则返回包含 ZonedSome

示例(安全地创建 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

如果输入字符串有效,函数返回包含 ZonedSome;如果输入无效,则返回 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
Why Use the Clock Service?

使用 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 值中较晚的那个。
isGreaterThanisGreaterThanOrEqualTo检查两个 DateTime 值之间的先后顺序。
between检查某个 DateTime 是否落在给定的边界范围内。
isFutureisPastisFutureUnsafe检查某个 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 上。
subtractDurationDateTime 中减去给定的 Duration
add把数值形式的各部分(例如 { hours: 2 })加到 DateTime 上。
subtract减去数值形式的各部分。
startOfDateTime 移动到给定单位的起点(例如一天的开始或一个月的开始)。
endOfDateTime 移动到给定单位的终点。
nearestDateTime 舍入到最近的指定单位。

格式化

函数说明
format使用 DateTimeFormat API 把 DateTime 格式化为字符串。
formatLocal使用系统本地时区和区域设置进行格式化。
formatUtc强制按 UTC 格式化。
formatIntl使用传入的 Intl.DateTimeFormat
formatIso返回 UTC 下的 ISO 8601 字符串。
formatIsoDate返回经过时区调整的 ISO 日期字符串。
formatIsoDateUtc返回 UTC 下的 ISO 日期字符串。
formatIsoOffsetZoned 格式化为带偏移量(形如 “+01:00”)的字符串。
formatIsoZonedYYYY-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")