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

DateTime

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

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

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

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

DateTime 类型

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

DateTime 有两种主要变体:

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

  2. Zoned:包含 epochMillis 以及一个 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 epochMillis: number
}

interface Zoned {
  readonly _tag: "Zoned"
  readonly epochMillis: 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 millis: number
    readonly seconds: number
    readonly minutes: number
    readonly hours: 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 是一种不可变结构,使用 epochMillis(自 Unix 纪元以来的毫秒数)表示协调世界时(UTC)中的一个时间点。

unsafeFromDate

从 JavaScript Date 创建一个 Utc。 如果提供的 Date 无效,则抛出 IllegalArgumentException

当传入一个 Date 对象时,它会被转换为 Utc 实例。该时间会被解释为执行代码的系统的本地时间,然后再调整为 UTC。这确保了日期和时间具有一致且与时区无关的表示。

示例(在意大利将本地时间转换为 UTC)

下面的示例假设代码在一个位于意大利(CET 时区)的系统上执行:

import { DateTime } from "effect"

// Create a Utc instance from a local JavaScript Date
//
//     ┌─── Utc
//     ▼
const utc = DateTime.unsafeFromDate(new Date("2025-01-01 04:00:00"))

console.log(utc)
// Output: DateTime.Utc(2025-01-01T03:00:00.000Z)

console.log(utc.epochMillis)
// Output: 1735700400000

解释

  • 本地时间 2025-01-01 04:00:00(意大利,CET)通过减去时区偏移量(1 月为 UTC+1)转换为 UTC
  • 因此,UTC 时间变为 2025-01-01 03:00:00.000Z
  • epochMillis 以自 Unix 纪元以来的毫秒数提供同一时间,确保 UTC 时间戳具有精确的数值表示。

unsafeMake

DateTime.Input 创建一个 Utc

示例(使用 unsafeMake 创建 DateTime)

下面的示例假设代码在一个位于意大利(CET 时区)的系统上执行:

import { DateTime } from "effect"

// From a JavaScript Date
const utc1 = DateTime.unsafeMake(new Date("2025-01-01 04:00:00"))
console.log(utc1)
// Output: DateTime.Utc(2025-01-01T03:00:00.000Z)

// From partial date parts
const utc2 = DateTime.unsafeMake({ year: 2025 })
console.log(utc2)
// Output: DateTime.Utc(2025-01-01T00:00:00.000Z)

// From a string
const utc3 = DateTime.unsafeMake("2025-01-01")
console.log(utc3)
// Output: DateTime.Utc(2025-01-01T00:00:00.000Z)

解释

  • 本地时间 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 } from "effect"

// From a JavaScript Date
const maybeUtc1 = DateTime.make(new Date("2025-01-01 04:00:00"))
console.log(maybeUtc1)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: '2025-01-01T03:00:00.000Z' }
*/

// From partial date parts
const maybeUtc2 = DateTime.make({ year: 2025 })
console.log(maybeUtc2)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: '2025-01-01T00:00:00.000Z' }
*/

// From a string
const maybeUtc3 = DateTime.make("2025-01-01")
console.log(maybeUtc3)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: '2025-01-01T00:00:00.000Z' }
*/

解释

  • 本地时间 2025-01-01 04:00:00(意大利,CET)通过减去时区偏移量(1 月为 UTC+1)转换为 UTC
  • 因此,UTC 时间变为 2025-01-01 03:00:00.000Z

Zoned 构造器

Zoned 包含 epochMillis 以及一个 TimeZone,让你可以为时间戳附加偏移量或命名区域(如 “America/New_York”)。

unsafeMakeZoned

通过将一个 DateTime.Input 与一个可选的 TimeZone 组合来创建 Zoned。 这让你能够表示一个带有相关联时区的特定时间点。

时区可以通过以下几种方式提供:

  • 作为一个 TimeZone 对象
  • 一个字符串标识符(例如 "Europe/London"
  • 一个以毫秒为单位的数值偏移量

如果输入或时区无效,则抛出 IllegalArgumentException

示例(在不指定时区的情况下创建 Zoned DateTime)

下面的示例假设代码在一个位于意大利(CET 时区)的系统上执行:

import { DateTime } from "effect"

// Create a Zoned DateTime based on the system's local time zone
const zoned = DateTime.unsafeMakeZoned(new Date("2025-01-01 04:00:00"))

console.log(zoned)
// Output: DateTime.Zoned(2025-01-01T04:00:00.000+01:00)

console.log(zoned.zone)
// Output: TimeZone.Offset(+01:00)

这里使用了系统的时区(CET,1 月为 UTC+1)来创建 Zoned 实例。

示例(指定命名时区)

下面的示例假设代码在一个位于意大利(CET 时区)的系统上执行:

import { DateTime } from "effect"

// Create a Zoned DateTime with a specified named time zone
const zoned = DateTime.unsafeMakeZoned(new Date("2025-01-01 04:00:00"), {
  timeZone: "Europe/Rome",
})

console.log(zoned)
// Output: DateTime.Zoned(2025-01-01T04:00:00.000+01:00[Europe/Rome])

console.log(zoned.zone)
// Output: TimeZone.Named(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.unsafeMakeZoned(new Date("2025-01-01 04:00:00"), {
  timeZone: "Europe/Rome",
  adjustForTimeZone: true,
})

console.log(zoned)
// Output: DateTime.Zoned(2025-01-01T03:00:00.000+01:00[Europe/Rome])

console.log(zoned.zone)
// Output: TimeZone.Named(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")
}

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")
}

当前时间

now

Effect<Utc> 的形式提供当前 UTC 时间,使用的是 Clock 服务。

示例(获取当前 UTC 时间)

import { DateTime, Effect } from "effect"

const program = Effect.gen(function* () {
  //      ┌─── Utc
  //      ▼
  const currentTime = yield* DateTime.now
})
Why Use the Clock Service?

使用 Clock 服务可以确保时间在你的应用中保持一致,这在测试环境中尤其有用,因为 你可能需要控制或模拟当前时间。

unsafeNow

使用 Date.now() 立即获取当前 UTC 时间,而不使用 Clock 服务。

示例(立即获取当前 UTC 时间)

import { DateTime } from "effect"

//      ┌─── Utc
//      ▼
const currentTime = DateTime.unsafeNow()

类型守卫

操作说明
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")
  }
}

时区管理

操作说明
setZone通过应用给定的 TimeZone,从 DateTime 创建一个 Zoned
setZoneOffset使用一个固定的偏移量(毫秒)从 DateTime 创建一个 Zoned
setZoneNamed使用一个 IANA 时区标识符从 DateTime 创建一个 Zoned,如果无效则返回 None
unsafeSetZoneNamed使用一个 IANA 时区标识符从 DateTime 创建一个 Zoned,如果无效则抛出异常。
zoneUnsafeMakeNamed使用一个 IANA 时区标识符创建一个 TimeZone.Named,如果标识符无效则抛出异常。
zoneMakeNamed使用一个 IANA 时区标识符创建一个 TimeZone.Named,如果无效则返回 None
zoneMakeNamedEffect使用一个 IANA 时区标识符创建一个 Effect<TimeZone.Named, IllegalArgumentException>,如果无效则以 IllegalArgumentException 失败
zoneMakeOffset使用一个以毫秒为单位的数值偏移量创建一个 TimeZone.Offset
zoneMakeLocal从系统的本地时区创建一个 TimeZone.Named
zoneFromString尝试从字符串解析时区,如果无效则返回 None
zoneToString返回 TimeZone 的字符串表示。

示例(将时区应用到一个 DateTime)

import { DateTime } from "effect"

// Create a UTC DateTime
//
//     ┌─── Utc
//     ▼
const utc = DateTime.unsafeMake("2024-01-01")

// Create a named time zone for New York
//
//      ┌─── TimeZone.Named
//      ▼
const zoneNY = DateTime.zoneUnsafeMakeNamed("America/New_York")

// Apply it to the DateTime
//
//      ┌─── Zoned
//      ▼
const zoned = DateTime.setZone(utc, zoneNY)

console.log(zoned)
// Output: DateTime.Zoned(2023-12-31T19:00:00.000-05:00[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))
// Output: true

// Attempt to parse an IANA time zone
const namedZone = DateTime.zoneFromString("Europe/London")
console.log(Option.isSome(namedZone))
// Output: true

// Invalid input
const invalidZone = DateTime.zoneFromString("Invalid/Zone")
console.log(Option.isSome(invalidZone))
// Output: false

比较

操作说明
distance返回两个 DateTime 之间的差值(以毫秒为单位)。
distanceDurationEither根据先后顺序返回一个 LeftRight Duration
distanceDuration返回一个 Duration,表示两个时间相距多远。
min返回两个 DateTime 值中较早的那个。
max返回两个 DateTime 值中较晚的那个。
greaterThan, greaterThanOrEqualTo, etc.检查两个 DateTime 值之间的顺序。
between检查一个 DateTime 是否落在给定的边界内。
isFuture, isPast, unsafeIsFuture, etc.检查一个 DateTime 是在未来还是过去。

示例(求两个 DateTime 之间的距离)

import { DateTime } from "effect"

const utc1 = DateTime.unsafeMake("2025-01-01T00:00:00Z")
const utc2 = DateTime.add(utc1, { days: 1 })

console.log(DateTime.distance(utc1, utc2))
// Output: 86400000 (one day)

console.log(DateTime.distanceDurationEither(utc1, utc2))
/*
Output:
{
  _id: 'Either',
  _tag: 'Right',
  right: { _id: 'Duration', _tag: 'Millis', millis: 86400000 }
}
*/

console.log(DateTime.distanceDuration(utc1, utc2))
// Output: { _id: 'Duration', _tag: 'Millis', 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.unsafeMake("2024-01-01"),
  DateTime.zoneUnsafeMakeNamed("Europe/Rome"),
)

console.log(DateTime.getPart(zoned, "month"))
// Output: 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当前时区的服务标签。
setZoneCurrentDateTime 设置为使用当前时区。
withCurrentZone为 effect 提供指定的时区。
withCurrentZoneLocal为 effect 使用系统的本地时区。
withCurrentZoneOffset为 effect 使用一个固定的偏移量(毫秒)。
withCurrentZoneNamed使用一个命名时区标识符(例如 “Europe/London”)。
nowInCurrentZoneZoned 的形式获取所配置时区中的当前时间。
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)
}).pipe(DateTime.withCurrentZoneNamed("Europe/London"))

Effect.runFork(program)
/*
Example Output:
DateTime.Zoned(2025-01-06T18:36:38.573+00:00[Europe/London])
*/