Enhance date manipulations with Kotlin extensions

Thomas Martin
WhozApp
Published in
5 min readMay 16, 2024

Like most business apps, we make extensive use of dates at Whoz, and we wrote a dozen useful extension methods on LocalDate that I will share with you in this article.

Generated with AI

Kotlin Extension Functions

All projects have utility classes and methods, avoiding copy-pasting code for commonly used functions. While Kotlin extension functions have several purposes, they are quite handy to replace the traditional utility classes. They avoid repeating the utility class name in your business code and are suggested by the IDE which encourages their use by the developers instead of an accidental code duplication.

Here are all our LocalDate extensions :

fun LocalDate.max(other: LocalDate) = LocalDateExtensions.max(this, other)
fun LocalDate.min(other: LocalDate) = LocalDateExtensions.min(this, other)
fun LocalDate.datesBetween(other: LocalDate) = LocalDateExtensions.datesBetween(this, other)
fun LocalDate.datesBetweenSequence(other: LocalDate) = LocalDateExtensions.datesBetweenSequence(this, other)
fun LocalDate.datesUntil(other: LocalDate) = LocalDateExtensions.datesUntil(this, other)
fun LocalDate.datesUntilSequence(other: LocalDate) = LocalDateExtensions.datesUntilSequence(this, other)
fun LocalDate.firstDayOfMonthsBetween(other: LocalDate) = LocalDateExtensions.firstDayOfMonthsBetween(this, other)
fun LocalDate.firstDayOfMonthsBetweenSequence(other: LocalDate) = LocalDateExtensions.firstDayOfMonthsBetweenSequence(this, other)
fun LocalDate.firstDayOfWeeksBetweenSequence(other: LocalDate) = LocalDateExtensions.firstDayOfWeeksBetweenSequence(this, other)
fun LocalDate.spreadTimespanIntoMonths(other: LocalDate) = LocalDateExtensions.spreadTimespanIntoMonths(this, other)
fun LocalDate.spreadTimespanIntoWeeks(other: LocalDate) = LocalDateExtensions.spreadTimespanIntoWeeks(this, other)
fun LocalDate.toInstant() = LocalDateExtensions.toInstant(this)
fun LocalDate.toUtcInstant() = LocalDateExtensions.toUtcInstant(this)
fun LocalDate.workingDaysBetween(
end: LocalDate,
ignore: List<DayOfWeek>,
publicHolidays: List<LocalDate>
) = LocalDateExtensions.workingDaysBetween(this, end, ignore, publicHolidays)

fun LocalDate.workingDaysBetween(
end: LocalDate,
publicHolidays: List<LocalDate>
) = LocalDateExtensions.workingDaysBetween(this, end, publicHolidays)


object LocalDateExtensions {

@JvmStatic
fun max(one: LocalDate, other: LocalDate) = if (one > other) one else other

@JvmStatic
fun min(one: LocalDate, other: LocalDate) = if (one < other) one else other

...
}

You can notice that all extensions have no direct implementation, and reference a method of the LocalDateExtension class, or more accurately object. This is useless if you are working on a full Kotlin project, but it allows one to have a static version like a classic Java utility class and so be callable from a Java class or a Groovy one.

Dates between

In this first set of methods, we provide developers with intuitive and efficient ways to handle date ranges


@JvmStatic
fun datesBetween(startDate: LocalDate, endDate: LocalDate): List<LocalDate> =
datesBetweenSequence(startDate, endDate).toList()

@JvmStatic
fun datesBetweenSequence(startDate: LocalDate, endDate: LocalDate): Sequence<LocalDate> =
generateSequence(startDate) { it.plusDays(1) }
.takeWhile { it <= endDate }

@JvmStatic
fun datesUntil(startDate: LocalDate, endDate: LocalDate): List<LocalDate> = datesUntilSequence(startDate, endDate).toList()

@JvmStatic
fun datesUntilSequence(startDate: LocalDate, endDate: LocalDate): Sequence<LocalDate> = datesBetweenSequence(startDate, endDate.minusDays(1))

While Java’s LocalDate.datesUntilmethod provides similar functionality, it returns a Stream<LocalDate> which is not idiomatic in Kotlin. So we wrote the datesUntil method to return a List<LocalDate> . We also created a datesBetween method to include the end date in the result.

Each method is complemented by a sequence-based counterpart, leveraging Kotlin’s powerful lazy evaluation feature. This is particularly useful when working with large date ranges, as it avoids the overhead of creating an entire list in memory. By providing both List and Sequence options, developers can choose the most appropriate tool for their context, enhancing performance and encouraging the use of more advanced Kotlin features.

First Day of Months and Weeks Between Dates

Our customers need data scattered around periods of weeks or months to do efficiently their staffing. It’s a common need for most business apps. Kotlin’s standard library doesn’t provide a direct way to do this, so we’ve created a set of methods to fill this gap.
The firstDayOfMonthsBetweenmethod returns a list of LocalDate containing the first day of each month between two dates.
Similarly, firstDayOfWeeksBetween generates a list of dates representing the first day of each week between two dates, defaulting to Monday as the start of the week. Again those methods have a sequence version.

Finally, the spreadTimespan methods are similar but also gives the end date of the wanted periods, represented as a list of pairs of LocalDate.

Here’s how these methods look in code:


@JvmStatic
fun firstDayOfMonthsBetween(startDate: LocalDate, endDate: LocalDate): List<LocalDate> = firstDayOfMonthsBetweenSequence(startDate, endDate).toList()

@JvmStatic
fun firstDayOfMonthsBetweenSequence(startDate: LocalDate, endDate: LocalDate): Sequence<LocalDate> = generateSequence(
startDate.withDayOfMonth(1)
) { it.plusMonths(1) }
.takeWhile { it <= endDate }

@JvmStatic
fun firstDayOfWeeksBetween(startDate: LocalDate, endDate: LocalDate): List<LocalDate> = firstDayOfWeeksBetweenSequence(startDate, endDate).toList()

@JvmStatic
fun firstDayOfWeeksBetweenSequence(startDate: LocalDate, endDate: LocalDate): Sequence<LocalDate> = generateSequence(
startDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
) { it.plusWeeks(1) }
.takeWhile { it <= endDate }

@JvmStatic
fun spreadTimespanIntoMonths(startDate: LocalDate, endDate: LocalDate): List<Pair<LocalDate, LocalDate>> = firstDayOfMonthsBetweenSequence(startDate, endDate)
.map { it to it.with(TemporalAdjusters.lastDayOfMonth()) }
.toList()

@JvmStatic
fun spreadTimespanIntoWeeks(startDate: LocalDate, endDate: LocalDate): List<Pair<LocalDate, LocalDate>> = firstDayOfWeeksBetweenSequence(startDate, endDate)
.map { it to it.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)) }
.toList()

Converting LocalDate to Instant

We’ve come a long way since that CalendarAPI before Java 8, but date and time zone handling can still be verbose. Our Kotlin extensions provide two methods for concise converting to instants: toInstantconverts a LocalDate to an Instant using the system’s default time zone, while toUtcInstant does the conversion using UTC. These methods simplify the conversion process and ensure consistency across different parts of the application that may require time zone-aware date operations.


@JvmStatic
fun toInstant(toConvert: LocalDate): Instant = toConvert.atStartOfDay(ZoneId.systemDefault()).toInstant()

@JvmStatic
fun toUtcInstant(toConvert: LocalDate): Instant = toConvert.atStartOfDay(ZoneId.of("UTC")).toInstant()

Calculating Working Days

Accurately calculating the number of working days between two dates is a common requirement for business applications, especially when accounting for weekends and public holidays. Our Kotlin extensions introduce workingDaysBetween methods to address this need.


/**
* @param start start date of the period
* @param end end date of the period
* @param ignore which days of week to ignore. ie.: mondays, or mondays and wednesdays..
* @return the number of work days between the two dates, limits included. Return 0 if end is before start
*/
@JvmStatic
fun workingDaysBetween(
start: LocalDate,
end: LocalDate,
ignore: List<DayOfWeek>,
publicHolidays: List<LocalDate>
): Int = if (end.isBefore(start)) {
0
} else {
val totalDays = ChronoUnit.DAYS.between(start, end) + 1
val totalWeeks = totalDays / 7
val remainingDays = totalDays % 7
var workingDays = totalDays - (totalWeeks * ignore.size)

for (i in 0 until remainingDays) {
if (start.plusDays(i).dayOfWeek in ignore) {
--workingDays
}
}

val countHolidays = publicHolidays.count { it.dayOfWeek !in ignore && !it.isBefore(start) && !it.isAfter(end) }

(workingDays - countHolidays).toInt()
}

/**
* @param start start date of the period
* @param end end date of the period
* @return
*/
@JvmStatic
fun workingDaysBetween(
start: LocalDate,
end: LocalDate,
publicHolidays: List<LocalDate>
): Int = workingDaysBetween(start, end, WEEKEND_DAYS, publicHolidays)

@JvmStatic
val WEEKEND_DAYS: List<DayOfWeek> = listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)

Take a close look at the implementation here. Our initial implementation of the workingDaysBetweenmethod operated in O(n) time complexity, iterating over each day in the range to determine the number of working days. This approach, while straightforward, became a performance bottleneck as observed through our Datadog monitoring, particularly in a microservice dealing with extensive date ranges.
To address this, we optimized the method to achieve O(1) complexity. The new implementation calculates the total number of days, then deducts the weekends in bulk by computing the number of complete weeks within the range. It further adjusts for any remaining days and public holidays without iterating over each day. This change significantly reduced CPU usage and improved the performance of the affected microservice.

We’ve delved into Kotlin’s powerful date manipulation extensions, from slicing time periods to streamlining working day calculations. The workingDaysBetween function stands out, transforming a CPU-intensive process into an efficient O(1) operation. Of course, this optimization was spread app-wide right away because extension functions avoid duplicating implementations.

We’re eager to hear how they boost your projects! Share your experiences, questions, or suggestions in the comments below. Let’s continue to refine our craft together. Happy coding!

--

--