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

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

ini 复制代码
var interval: Duration = 1.seconds

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

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

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() // 系统默认

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

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 提供了 DAYWEEKMONTHYEARHOURMINUTESECONDMILLISECOND 等单位,覆盖大部分场景。如果需要一次性加减多个单位,可以用 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)

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

ini 复制代码
val berlin = TimeZone.of("Europe/Berlin")
val startOfDay = LocalDate(2024, 3, 31).atStartOfDayIn(berlin)
相关推荐
装杯让你飞起来啊3 小时前
第 4 周 Unit 2:Jetpack Compose 状态、按钮、计数器与小费计算器
windows·microsoft·kotlin·安卓
Kapaseker8 小时前
MVVM 旧城改造,边界划分各有招
android·kotlin
装杯让你飞起来啊1 天前
第 2 周 Day 5-6:综合小游戏 —— 学生成绩管理系统
windows·microsoft·kotlin
装杯让你飞起来啊1 天前
Kotlin List / Array 与 for 循环
开发语言·kotlin·list
装杯让你飞起来啊1 天前
混合练习 —— 猜数字游戏
windows·游戏·kotlin
装杯让你飞起来啊1 天前
Kotlin 条件判断 if / when 与智能转换 smart cast
开发语言·python·kotlin
pengyu1 天前
【Kotlin 协程修仙录 · 金丹境 · 初阶】 | 并发艺术:async/await 与并发组合的优雅之道
android·kotlin
黄林晴1 天前
重磅发布!KMP 双端订阅支付彻底封神,一套代码搞定 iOS+Android
android·kotlin
alexhilton2 天前
揭密:Compose应用如何做到启动提升34%
android·kotlin·android jetpack