Kotlin日期相关处理

前言

在 Kotlin(基于 JVM)中,日期时间处理主要依赖 Java 8 引入的 java.time(JSR-310),它取代了老旧且问题较多的 java.util.DateCalendar

Kotlin 本身不提供新的日期类,但能以更简洁、安全的方式使用这些 Java 类。

核心类包括:

  • LocalDate:仅日期(年-月-日),无时区。
  • YearMonth:仅日期(年-月),无时区。
  • LocalTime:仅时间(时-分-秒-纳秒),无时区。
  • LocalDateTime:日期 + 时间,无时区。
  • ZonedDateTime:日期 + 时间 + 时区(含夏令时规则)。
  • OffsetDateTime:日期 + 时间 + UTC 偏移量(无时区规则)。
  • Instant:UTC 时间戳(自 1970-01-01T00:00:00Z 起的瞬时点)。
  • Duration:基于秒/纳秒的时间量(如 2 小时)。
  • Period:基于年-月-日的日历量(如 1 年 3 个月)。
  • DateTimeFormatter:线程安全的格式化与解析工具。
  • ZoneId / ZoneOffset:表示时区或偏移量。

遗留类如 java.util.DateCalendarSimpleDateFormat 已不推荐使用。

日期 <=> 字符串

方式 优点 缺点
SimpleDateFormat 简单直接,兼容老代码 非线程安全,API 设计老旧
java.time + Date.from() / Date.toInstant() 线程安全、功能强大、可读性好 代码稍长,需理解新时间 API

日期 <=> 字符串

使用java.time(推荐)

Date => 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.*

fun dateToStringModern(date: Date, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
val localDateTime = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
return localDateTime.format(DateTimeFormatter.ofPattern(pattern))
}

fun main() {
val date = Date()
println(dateToStringModern(date)) // 2025-12-08 10:30:45
println(dateToStringModern(date, "yyyy-MM-dd")) // 2025-12-08
}

字符串 => Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.*

fun stringToDateModern(str: String, pattern: String = "yyyy-MM-dd HH:mm:ss"): Date? {
return try {
val formatter = DateTimeFormatter.ofPattern(pattern)
val localDateTime = LocalDateTime.parse(str, formatter)
val instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant()
Date.from(instant)
} catch (e: Exception) {
null
}
}

fun main() {
val date = stringToDateModern("2025-12-08 14:30:00")
println(date)
}

使用SimpleDateFormat

Date => 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.text.SimpleDateFormat
import java.util.*

fun dateToString(date: Date, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
val sdf = SimpleDateFormat(pattern, Locale.getDefault())
return sdf.format(date)
}

fun main() {
val date = Date()
println(dateToString(date)) // 2025-12-08 10:30:45
println(dateToString(date, "yyyy/MM/dd")) // 2025/12/08
}

字符串 => Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.text.SimpleDateFormat
import java.util.*

fun stringToDate(str: String, pattern: String = "yyyy-MM-dd HH:mm:ss"): Date? {
return try {
val sdf = SimpleDateFormat(pattern, Locale.getDefault())
sdf.timeZone = TimeZone.getDefault() // 可选:显式设置时区
sdf.parse(str)
} catch (e: Exception) {
null // 或抛出异常
}
}

fun main() {
val date = stringToDate("2025-12-08 14:30:00")
println(date) // Mon Dec 08 14:30:00 CST 2025(具体时区取决于系统)
}

年月 <=> 字符串

年月 => 字符串

1
2
3
val yearMonth = YearMonth.now() // 当前年月
val yearMonthStr = yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))
println(yearMonthStr) // 2025-12

字符串 => 年月

1
2
3
val defaultMonth = "2025-06"
val formatter = DateTimeFormatter.ofPattern("yyyy-MM")
val yearMonth = YearMonth.parse(defaultMonth, formatter)

获取某日所在周起止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.DateTimeFormatter

/**
* 获取某天所在周周一
*/
fun LocalDate.weekStartMonday(): LocalDate {
val daysSinceMonday = this.dayOfWeek.value - DayOfWeek.MONDAY.value // MONDAY.value = 1
return this.minusDays(daysSinceMonday.toLong())
}

/**
* 获取某天所在周周日
*/
fun LocalDate.weekEndSunday(): LocalDate {
// 当前是星期几(MONDAY=1, ..., SUNDAY=7)
val daysToAdd = DayOfWeek.SUNDAY.value - this.dayOfWeek.value
return this.plusDays(daysToAdd.toLong())
}

获取某月起止

1
2
3
4
5
6
7
8
9
10
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.DateTimeFormatter

fun getFirstAndLastDay(dateStr: String, pattern: String = "yyyy-MM"): Pair<LocalDate, LocalDate> {
val formatter = DateTimeFormatter.ofPattern(pattern)
val yearMonth = YearMonth.parse(dateStr, formatter)
return yearMonth.atDay(1) to yearMonth.atEndOfMonth()
}

使用

1
2
val dateStr = "2025-06"
val (f, l) = getFirstAndLastDay(dateStr)

获取两个日期之间的所有日期

1
2
3
4
5
6
7
8
9
val dateStr = "2025-06"
val (firstDay, lastDay) = getFirstAndLastDay(dateStr)

val allDayList = mutableListOf<LocalDate>()
var tempDay = firstDay
while (!tempDay.isAfter(lastDay)) {
allDayList.add(tempDay)
tempDay = tempDay.plusDays(1)
}

LocalDate

LocalDate 是 Java 8 引入的 java.time 包中用于表示不带时间、不带时区的日期(年-月-日) 的不可变类。

它在 Kotlin 中使用非常自然,API 清晰且线程安全。

以下是 LocalDate 常用方法及示例(Kotlin 语法):

创建 LocalDate

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.time.LocalDate

// 当前日期(系统默认时区)
val today = LocalDate.now()

// 指定年月日
val date1 = LocalDate.of(2025, 12, 8)

// 从字符串解析(ISO 格式 yyyy-MM-dd)
val date3 = LocalDate.parse("2025-12-08")

// 从年份和一年中的第几天创建
val date4 = LocalDate.ofYearDay(2025, 342) // 2025 年的第 342 天

指定格式

1
2
3
4
val str2 = "2025年12月08日"
val formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日")
val date2 = LocalDate.parse(str2, formatter2)
println(date2) // 2025-12-08

格式化为字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.time.format.DateTimeFormatter

val date = LocalDate.of(2025, 12, 8)

// 默认 ISO 格式(yyyy-MM-dd)
println(date.toString()) // "2025-12-08"

// 自定义格式
val formatter1 = DateTimeFormatter.ofPattern("yyyy年MM月dd日")
println(date.format(formatter1)) // "2025年12月08日"

val formatter2 = DateTimeFormatter.ofPattern("dd/MM/yyyy")
println(date.format(formatter2)) // "08/12/2025"

// 使用本地化格式(如中文)
val chineseFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.CHINA)
println(date.format(chineseFormatter)) // "2025-12-8"(根据区域习惯)

获取字段(年、月、日等)

1
2
3
4
5
6
7
8
9
10
val date = LocalDate.of(2025, 12, 8)

println(date.year) // 2025
println(date.monthValue) // 12(数字)
println(date.month) // DECEMBER(枚举)
println(date.dayOfMonth) // 8
println(date.dayOfWeek) // MONDAY(枚举,周一为第一天)
println(date.dayOfYear) // 342(2025-12-08 是当年第 342 天)
println(date.lengthOfMonth())// 31(12月有31天)
println(date.isLeapYear) // false(2025 不是闰年)

日期计算(加减)

所有操作返回新对象(不可变):

1
2
3
4
5
6
7
8
9
10
11
12
13
val date = LocalDate.of(2025, 12, 8)

// 加
val tomorrow = date.plusDays(1) // 2025-12-09
val nextMonth = date.plusMonths(1) // 2026-01-08
val nextYear = date.plusYears(1) // 2026-12-08

// 减
val yesterday = date.minusDays(1) // 2025-12-07
val lastMonth = date.minusMonths(2) // 2025-10-08

// 使用 Period(更复杂周期)
val inTwoWeeks = date.plus(java.time.Period.ofWeeks(2)) // +14 天

比较日期

1
2
3
4
5
6
7
8
9
10
11
12
val d1 = LocalDate.of(2025, 12, 8)
val d2 = LocalDate.of(2025, 12, 10)

println(d1.isBefore(d2)) // true
println(d1.isAfter(d2)) // false
println(d1.isEqual(d2)) // false
println(d1.compareTo(d2)) // -1(可用于排序)

// 判断是否在某个范围
if (d1 in d2.minusDays(5)..d2.plusDays(5)) {
println("d1 在 d2 前后5天内")
}

💡 LocalDate 实现了 Comparable,可直接用于 sorted()min()max() 等。

调整日期(withXXX)

1
2
3
4
5
6
7
8
9
10
val date = LocalDate.of(2025, 12, 8)

val firstDayOfMonth = date.withDayOfMonth(1) // 2025-12-01
val lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth()) // 2025-12-31

// 下一个周一
val nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY))

// 本月第一天
val startOfMonth = date.with(TemporalAdjusters.firstDayOfMonth())

需要导入:import java.time.temporal.TemporalAdjusters

与其它时间类型互转(简要)

1
2
3
4
5
6
7
8
9
// LocalDate → LocalDateTime(加上时间,默认 00:00)
val ldt = date.atStartOfDay() // 2025-12-08T00:00

// LocalDate → Date(需指定时区)
val utilDate = Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant())

// String → LocalDate(非 ISO 格式)
val customStr = "08-12-2025"
val parsed = LocalDate.parse(customStr, DateTimeFormatter.ofPattern("dd-MM-yyyy"))

小贴士

  • LocalDate 没有时区概念,适合表示生日、节假日、合同起止日等。
  • 所有方法都是不可变操作,不会修改原对象。
  • 推荐配合 DateTimeFormatterTemporalAdjusters 使用高级功能。