之前处理时间间隔全靠 Long 加注释,读代码时经常搞不清参数含义。后来发现 kotlin.time.Duration(Kotlin 标准库自带)配合 kotlinx-datetime 用,体验好很多:
ini
var interval: Duration = 1.seconds
作为函数参数时语义也更明确------之前踩过坑,同样叫 time 的参数,有的是时间戳 Instant,有的是时间间隔 Duration,不看代码根本分不清。
kotlinx-datetime 在 Duration 之上提供了 Instant 的算术运算,Instant 和 Duration 之间可以互相加减:
ini
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:
ini
val now: Instant = Clock.System.now()
val today: LocalDate = Clock.System.todayIn(TimeZone.of("Asia/Shanghai"))
解析 ISO 8601 字符串也直观:
ini
val instant = Instant.parse("2020-01-01T00:00:00Z")
val localDateTime = LocalDateTime.parse("2021-03-27T02:16:20")
时区与转换
创建时区的方式比较灵活:
ini
val zone1 = TimeZone.of("Europe/Berlin") // 用 ID
val zone2 = TimeZone.of("UTC+5") // 用偏移量
val utc = TimeZone.UTC // UTC
val local = TimeZone.currentSystemDefault() // 系统默认
Instant 转 LocalDateTime 只需要指定时区,反过来也一样:
ini
// Instant → LocalDateTime
val localNow = now.toLocalDateTime(TimeZone.currentSystemDefault())
// LocalDateTime → Instant(需要时区,因为 DST 可能导致歧义)
val instant = localDateTime.toInstant(TimeZone.of("Asia/Shanghai"))
以前写过一个扩展函数来做这个转换,现在直接用库提供的方法就够了:
kotlin
// 之前的写法
fun Timestamp.toLocalDateTime(timeZone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime =
Instant.fromEpochMilliseconds(this.value).toLocalDateTime(timeZone)
// 现在直接用
val localDateTime = instant.toLocalDateTime(timeZone)
解析与格式化
默认情况下 toString() 输出 ISO 8601 格式,parse 反向解析:
ini
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 构建器定义自己的格式:
scss
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 用于格式化输出------同一个格式器对象两个方向都能用。
时间算术
时间算术分两种情况。基于小时、分钟这种固定长度的单位,不需要时区:
ini
val later = instant + 5.hours
val in30Min = instant + 30.minutes
基于天、月、年这种日期感知的单位,必须传时区------因为夏令时切换会导致"一天"不一定是 24 小时:
ini
val tomorrow = instant.plus(1, DateTimeUnit.DAY, TimeZone.of("Asia/Shanghai"))
val nextMonth = instant.plus(1, DateTimeUnit.MONTH, timeZone)
DateTimeUnit 提供了 DAY、WEEK、MONTH、YEAR、HOUR、MINUTE、SECOND、MILLISECOND 等单位,覆盖大部分场景。如果需要一次性加减多个单位,可以用 DateTimePeriod:
ini
val period = DateTimePeriod(days = 1, hours = 3, minutes = 30)
val result = instant.plus(period, timeZone)
计算两个时间点之间的间隔也方便:
ini
val daysDiff = pastInstant.daysUntil(now, timeZone)
val monthsDiff = pastInstant.monthsUntil(now, timeZone)
夏令时陷阱
这是实际使用中容易踩的坑。给 LocalDateTime 直接加天数,遇到 DST 切换时可能跳过或重复小时。稳妥做法是先转成 Instant 再操作:
scss
// 有风险:02:16:20 在 DST 切换日可能不存在
val risky = localDateTime.plus(1, DateTimeUnit.DAY)
// 更安全:通过 Instant 做日期感知运算
val safe = localDateTime.toInstant(timeZone)
.plus(1, DateTimeUnit.DAY, timeZone)
.toLocalDateTime(timeZone)
LocalDate 的 atStartOfDayIn 方法也能正确处理 DST 开始日:
ini
val berlin = TimeZone.of("Europe/Berlin")
val startOfDay = LocalDate(2024, 3, 31).atStartOfDayIn(berlin)