使用datetime更加优雅地在kotlin中处理时间

18 阅读3分钟

之前处理时间间隔全靠 Long 加注释,读代码时经常搞不清参数含义。后来发现 kotlin.time.Duration(Kotlin 标准库自带)配合 kotlinx-datetime 用,体验好很多:

var interval: Duration = 1.seconds

作为函数参数时语义也更明确——之前踩过坑,同样叫 time 的参数,有的是时间戳 Instant,有的是时间间隔 Duration,不看代码根本分不清。

kotlinx-datetime 在 Duration 之上提供了 Instant 的算术运算,InstantDuration 之间可以互相加减:

val future = instant + 5.hours       // Instant ± Duration → Instant
val total = 2.hours + 45.minutes     // Duration ± Duration → Duration

Instant 基础

Instant 有两个常用的常量:Instant.DISTANT_FUTURE(遥远的未来)和 Instant.DISTANT_PAST(遥远的过去),设默认值或边界条件时挺好用。

获取当前时间直接用 Clock.System

val now: Instant = Clock.System.now()
val today: LocalDate = Clock.System.todayIn(TimeZone.of("Asia/Shanghai"))

解析 ISO 8601 字符串也直观:

val instant = Instant.parse("2020-01-01T00:00:00Z")
val localDateTime = LocalDateTime.parse("2021-03-27T02:16:20")

时区与转换

创建时区的方式比较灵活:

val zone1 = TimeZone.of("Europe/Berlin")   // 用 ID
val zone2 = TimeZone.of("UTC+5")           // 用偏移量
val utc = TimeZone.UTC                      // UTC
val local = TimeZone.currentSystemDefault() // 系统默认

InstantLocalDateTime 只需要指定时区,反过来也一样:

// Instant → LocalDateTime
val localNow = now.toLocalDateTime(TimeZone.currentSystemDefault())

// LocalDateTime → Instant(需要时区,因为 DST 可能导致歧义)
val instant = localDateTime.toInstant(TimeZone.of("Asia/Shanghai"))

以前写过一个扩展函数来做这个转换,现在直接用库提供的方法就够了:

// 之前的写法
fun Timestamp.toLocalDateTime(timeZone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime =
    Instant.fromEpochMilliseconds(this.value).toLocalDateTime(timeZone)

// 现在直接用
val localDateTime = instant.toLocalDateTime(timeZone)

解析与格式化

默认情况下 toString() 输出 ISO 8601 格式,parse 反向解析:

val localDateTime = LocalDateTime(2025, 3, 21, 12, 27, 35, 124365453)
localDateTime.toString()  // 2025-03-21T12:27:35.124365453
val same = LocalDateTime.parse("2025-03-21T12:27:35.124365453")

但实际项目中经常需要非标准格式(比如从接口拿到 03/24 2023 这种字符串),这时候用 Format 构建器定义自己的格式:

import kotlinx.datetime.format.*

val dateFormat = LocalDate.Format {
    monthNumber(padding = Padding.SPACE)
    char('/')
    day()
    char(' ')
    year()
}

val date = dateFormat.parse("12/24 2023")
println(date.format(LocalDate.Formats.ISO_BASIC)) // "20231224"

parse 用于解析字符串,format 用于格式化输出——同一个格式器对象两个方向都能用。

时间算术

时间算术分两种情况。基于小时、分钟这种固定长度的单位,不需要时区:

val later = instant + 5.hours
val in30Min = instant + 30.minutes

基于天、月、年这种日期感知的单位,必须传时区——因为夏令时切换会导致"一天"不一定是 24 小时:

val tomorrow = instant.plus(1, DateTimeUnit.DAY, TimeZone.of("Asia/Shanghai"))
val nextMonth = instant.plus(1, DateTimeUnit.MONTH, timeZone)

DateTimeUnit 提供了 DAYWEEKMONTHYEARHOURMINUTESECONDMILLISECOND 等单位,覆盖大部分场景。如果需要一次性加减多个单位,可以用 DateTimePeriod

val period = DateTimePeriod(days = 1, hours = 3, minutes = 30)
val result = instant.plus(period, timeZone)

计算两个时间点之间的间隔也方便:

val daysDiff = pastInstant.daysUntil(now, timeZone)
val monthsDiff = pastInstant.monthsUntil(now, timeZone)

夏令时陷阱

这是实际使用中容易踩的坑。给 LocalDateTime 直接加天数,遇到 DST 切换时可能跳过或重复小时。稳妥做法是先转成 Instant 再操作:

// 有风险:02:16:20 在 DST 切换日可能不存在
val risky = localDateTime.plus(1, DateTimeUnit.DAY)

// 更安全:通过 Instant 做日期感知运算
val safe = localDateTime.toInstant(timeZone)
    .plus(1, DateTimeUnit.DAY, timeZone)
    .toLocalDateTime(timeZone)

LocalDateatStartOfDayIn 方法也能正确处理 DST 开始日:

val berlin = TimeZone.of("Europe/Berlin")
val startOfDay = LocalDate(2024, 3, 31).atStartOfDayIn(berlin)