DateTime
使用 Effect 的 DateTime 处理精确的时间点,支持创建、比较和算术运算,从而高效地处理时间。
在 JavaScript 中处理日期和时间可能颇具挑战。内置的 Date 对象会修改自身的内部状态,时区处理也可能令人困惑。这些设计选择会在开发依赖日期时间准确性的应用时引入错误,例如调度系统、时间戳服务或日志工具。
DateTime 模块旨在通过提供以下特性来解决这些局限:
- 不可变数据:每个
DateTime都是不可变结构,可减少与就地修改相关的错误。 - 时区支持:
DateTime为时区提供了完善的支持,包括自动处理夏令时调整。 - 算术运算:你可以对
DateTime实例执行算术运算,例如加上或减去一个时长(duration)。
DateTime 类型
DateTime 表示时间中的一个时刻。它既可以存储为简单的 UTC 值,也可以存储为带有关联时区的值。以这种方式存储时间,有助于你同时管理精确的时间戳,以及该时间应如何显示或解释的上下文。
DateTime 有两种主要变体:
-
Utc:一种不可变结构,使用
epochMillis(自 Unix 纪元以来的毫秒数)表示协调世界时(UTC)中的一个时间点。 -
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。如果有效,则返回包含 Utc 的 Some。
示例(安全地创建 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。如果有效,则返回包含 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")
}
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")
}
当前时间
now
以 Effect<Utc> 的形式提供当前 UTC 时间,使用的是 Clock 服务。
示例(获取当前 UTC 时间)
import { DateTime, Effect } from "effect"
const program = Effect.gen(function* () {
// ┌─── Utc
// ▼
const currentTime = yield* DateTime.now
})
使用 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 | 根据先后顺序返回一个 Left 或 Right 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 上。 |
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)
}).pipe(DateTime.withCurrentZoneNamed("Europe/London"))
Effect.runFork(program)
/*
Example Output:
DateTime.Zoned(2025-01-06T18:36:38.573+00:00[Europe/London])
*/