Kotlinx DateTime manipulation for KMP

Raed Ghazal
6 min readOct 7, 2023

--

Working with dates and times in a Kotlin Multiplatform (KMP) can be difficult as we don’t have access to java.time package, which is unfortunate as java.time.LocalDateTime is so powerful and has great support, while moving to kotlinx.datetime.LocalDateTime can feel like a hard task.

This article will be your full guide to working with and manipulating LocalDateTime , LocalDate , LocalTime from kotlinx.datetime library and all the needed functionality, especially if you’re migrating from a project that uses java.time package!

✨Update: Here is Some NEW Good News✨

I got some feedback on why not create a library that contains all the below functionality and implementation for easier access to anyone interested in having those extension functions instead of each one implementing them on their own.

So I have created a `kotlinx.datetime` extension library, and there is no need to follow the below guide on how to implement all the functions, just check my Library GitHub repo and get quick access to all of them and more with zero effort and boilerplate code on your side!

*All the details are mentioned in the repo’s Readme file

https://github.com/RaedGhazal/kotlinx-datetime-ext

*You can continue reading if you want to know the details behind the implementation

Continue to the implementation details that the library was built on

Dependency implementation

To start using kotlinx datetime, write the following in (app) build.gradle and in commonMain dependencies if you’re working with KMP

implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")

Getting the current date and time

To get the current date and time in kotlinx.datetime library it's not so intuitive as java.time package, where we used to just write LocalDateTime.now() .

but in Kotlin’s we have to use the system Clock and default TimeZone by writing:

Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())

and then we can use the getters .date and .time to get the LocalDate and LocalTime instances.

To make it easier though, we can create some extension functions:

import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone
fun LocalDateTime.Companion.now(): LocalDateTime {
return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
}
fun LocalDate.Companion.now(): LocalDate {
return LocalDateTime.now().date
}
fun LocalTime.Companion.now(): LocalTime {
return LocalDateTime.now().time
}

then by simple writing LocalDateTime.now() , LocalDate.now() and LocalTime.now() we get quick access to now’s date or time object.

Tweaks and date-time manipulation

When working with date time we often need quick reach to other functions as well, like getting an instance of the minimum time of the day and maximum time of the day, especially when working with ranges, like yesterday .. now in this case, at what time would yesterday be? probably the beginning of the day.

To create our own extension methods, .min() would be easy, because it would be LocalTime(hours = 0, minutes = 0) but for .max() I had to check what values java.time use, and found this LocalTime(hours = 23, minutes = 59, seconds = 59, nanoseconds = 999999999)

so we can create those 2 extension functions

fun LocalTime.Companion.min(): LocalTime {
return LocalTime(0, 0)
}
fun LocalTime.Companion.max(): LocalTime {
return LocalTime(23, 59, 59, 999999999)
}

and we can use them to convert LocalDate instances to LocalDateTime instances of startOfDay and endOfDay

fun LocalDate.atStartOfDay(): LocalDateTime {
return LocalDateTime(this, LocalTime.min())
}
fun LocalDate.atEndOfDay(): LocalDateTime {
return LocalDateTime(this, LocalTime.max())
}

DateTime addition and subtraction

For addition and subtraction, unfortunately, we can’t do it directly from LocalDateTime instance, we have to convert it to Instant, do the calculation, and then convert it back to LocalDateTime, so here are some more extension functions

import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
fun LocalDateTime.plus(value: Long, unit: DateTimeUnit.TimeBased): LocalDateTime {
val timeZone = TimeZone.currentSystemDefault()
return this.toInstant(timeZone)
.plus(value, unit)
.toLocalDateTime(timeZone)
}
fun LocalDateTime.minus(value: Long, unit: DateTimeUnit.TimeBased): LocalDateTime {
val timeZone = TimeZone.currentSystemDefault()
return this.toInstant(timeZone)
.minus(value, unit)
.toLocalDateTime(timeZone)
}

here are some examples of usages:

val datetime: LocalDateTime = LocalDateTime.now()
val dateTimeAfterOneHour: LocalDateTime = datetime.plus(1, DateTimeUnit.HOUR)
val dateTimeAfterOneDay: LocalDateTime = datetime.plus(24, DateTimeUnit.HOUR)
val dateTime30MinEarlier: LocalDateTime = datetime.minus(30, DateTimeUnit.MINUTE)

Working with YearMonth

YearMonth class existed in java.time but there is no alternative in kotlinx.datetime and sometimes it's useful when we want to point out a month of a specific year and have some helper functions, like:

val yearMonth = YearMonth(2023, Month.APRIL)
val fifthOfMonth: LocalDateTime = yearMonth.atDay(5) // 2023/04/05//since it's not easy to know how many days this month holds, 
// it's nice to have a function that figures that out for you
val endOfMonth: LocalDateTime = yearMonth.atEndOfMonth()
// those are useful when working with ranges.
val twoMonthsEarlier: YearMonth = yearMonth.plusMonths(2)
val sevenMonthsForward: YearMonth = yearMonth.minusMonths(7)
//sample usage
val range = twoMonthsEarlier.atDay(0) .. sevenMonthsForward.atEndOfMonth()

For that, we can create our own YearMonth class that’s pure Kotlin with similar functions to java.time’s

import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month
import kotlinx.datetime.number
data class YearMonth(val year: Int, val month: Month) {
fun atDay(day: Int): LocalDate {
return LocalDate(year, month, day)
}
fun atEndOfMonth(): LocalDate {
val lastDay = month.number.monthLength(isLeapYear(year))
return LocalDate(year, month, lastDay)
}
fun plusMonths(months: Int): YearMonth {
val newMonth = month.number + months
val newYear = year + (newMonth - 1) / 12
val newMonthNumber = (newMonth - 1) % 12 + 1
return YearMonth(newYear, newMonthNumber.toMonth())
}
fun minusMonths(months: Int): YearMonth {
val totalMonths = year * 12 + (month.number - 1) - months
val newYear = totalMonths / 12
val newMonth = (totalMonths % 12) + 1 // Adding 1 because months are 1-based
return YearMonth(newYear, newMonth.toMonth())
}
private fun Int.toMonth(): Month {
return Month.values()[this - 1]
}
// the below 2 functions were taken from `java.time` package
private fun isLeapYear(year: Int): Boolean {
val prolepticYear: Long = year.toLong()
return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L)
}
private fun Int.monthLength(isLeapYear: Boolean): Int {
return when (this) {
2 -> if (isLeapYear) 29 else 28
4, 6, 9, 11 -> 30
else -> 31
}
}
companion object {
fun from(localDate: LocalDate): YearMonth {
return YearMonth(localDate.year, localDate.month)
}
}
}

Formatting and Parsing

Formatting and parsing don’t come out of the box with kotlinx.datetime library and we have to use platform-specific code for each platform to format and parse the dates we work with.

Formatting LocalDateTime to string

To format on the Android side:

import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toJavaLocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
actual fun format(localDateTime: LocalDateTime): String {
val str = DateTimeFormatter
.ofPattern("dd/MM/yyyy - HH:mm", Locale.getDefault())
.format(localDateTime.toJavaLocalDateTime())
}
// usage
val localDateTime = LocalDateTime(2023, 10, 7, 13, 10)
print(format(localDateTime))
// output: 07/10/2023 - 13:10

this code can’t be used in commonMain because it uses java DateTimeFormatter and Locale

However, on iOS it is a bit more difficult:

we have to first convert LocalDateTime to objective-c NSDate

private fun LocalDateTime.toNsDate(): NSDate? {
val calendar = NSCalendar.currentCalendar
val components = NSDateComponents()
components.year = this.year.convert()
components.month = this.monthNumber.convert()
components.day = this.dayOfMonth.convert()
components.hour = this.hour.convert()
components.minute = this.minute.convert()
components.second = this.second.convert()
return calendar.dateFromComponents(components)
}
actual fun format(localDateTime: LocalDateTime): String {
val date = localDateTime.toNsDate() ?: throw IllegalStateException("Failed to convert LocalDateTime $LocalDateTime to NSDate")
...
}

then create a NSDateFormatter with the specific pattern and locale

actual fun format(localDateTime: LocalDateTime): String {
val date = localDateTime.toNsDate() ?: throw IllegalStateException("Failed to convert LocalDateTime $LocalDateTime to NSDate")
val formatter = NSDateFormatter().apply {
dateFormat = "dd/MM/yyyy - HH:mm"
locale = NSLocale.currentLocale
} // <-
...
}

and then using .stringFromDate(date) to format the date

actual fun format(localDateTime: LocalDateTime): String {
val date = localDateTime.toNsDate() ?: throw IllegalStateException("Failed to convert LocalDateTime $LocalDateTime to NSDate")
val formatter = NSDateFormatter().apply {
dateFormat = pattern
locale = NSLocale.currentLocale
}
return formatter.stringFromDate(date) // <-
}

Parsing String to LocalDateTime

Here we also have different implementations for Android and iOS modules.

on Android:

actual fun parse(strDateTime: String): LocalDateTime {
val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy - HH:mm", Locale.getDefault())
return java.time.LocalDateTime
.parse(strDateTime, formatter)
.toKotlinLocalDateTime()
}

on iOS:

actual fun parse(str: String): LocalDateTime {
val formatter = NSDateFormatter().apply {
dateFormat = "dd/MM/yyyy - HH:mm"
locale = NSLocale.currentLocale
}
return formatter
.dateFromString(str)
// extensions functions provided by kotlinx.datetime
?.toKotlinInstant()
?.toLocalDateTime(TimeZone.currentSystemDefault())
?: throw IllegalStateException("Failed to convert String $str to LocalDateTime")
}

And that’s it, you are all good to go from here.

let me know in the comments if something is missing or if you’re struggling with other functionalities!

Show support

If you enjoyed the article, support me by leaving some claps (50 :) ), and follow me on Medium and social media 🙌

Social media

LinkedIn, Twitter, GitHub

References:

  1. https://github.com/Kotlin/kotlinx-datetime

--

--

Raed Ghazal
Raed Ghazal

Written by Raed Ghazal

Lead Android Engineer at Jodel, passionate about the mobile development industry and all the new things that come with it!

No responses yet