Scala - Date and Time Operations

The `java.time` package provides separate classes for dates, times, and combined date-times. Use `LocalDate` for calendar dates without time information and `LocalTime` for time without date context.

Key Insights

  • Scala’s java.time API (JSR-310) provides immutable, thread-safe date-time operations superior to legacy java.util.Date and java.util.Calendar classes
  • LocalDateTime, ZonedDateTime, and Instant serve distinct purposes: local operations without timezone context, timezone-aware calculations, and machine timestamps respectively
  • Proper timezone handling and duration calculations prevent common pitfalls in distributed systems, scheduling applications, and financial transactions

Working with LocalDate and LocalTime

The java.time package provides separate classes for dates, times, and combined date-times. Use LocalDate for calendar dates without time information and LocalTime for time without date context.

import java.time.{LocalDate, LocalTime, LocalDateTime}
import java.time.format.DateTimeFormatter

// Creating date instances
val today = LocalDate.now()
val specificDate = LocalDate.of(2024, 3, 15)
val parsedDate = LocalDate.parse("2024-03-15")

// Date arithmetic
val nextWeek = today.plusWeeks(1)
val lastMonth = today.minusMonths(1)
val firstDayOfMonth = today.withDayOfMonth(1)

// Working with time
val now = LocalTime.now()
val specificTime = LocalTime.of(14, 30, 0)
val lunchTime = LocalTime.parse("12:30:00")

// Combining date and time
val dateTime = LocalDateTime.of(specificDate, specificTime)
val currentDateTime = LocalDateTime.now()

println(s"Today: $today")
println(s"Next week: $nextWeek")
println(s"Current time: ${now.format(DateTimeFormatter.ofPattern("HH:mm:ss"))}")

Timezone-Aware Operations with ZonedDateTime

When working across timezones, ZonedDateTime maintains both the local time and timezone information. This is critical for scheduling systems, global applications, and compliance requirements.

import java.time.{ZonedDateTime, ZoneId}
import java.time.format.DateTimeFormatter

// Creating timezone-aware dates
val londonTime = ZonedDateTime.now(ZoneId.of("Europe/London"))
val newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"))
val tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))

// Converting between timezones
val londonToNewYork = londonTime.withZoneSameInstant(ZoneId.of("America/New_York"))

// Parsing with timezone
val formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME
val parsed = ZonedDateTime.parse("2024-03-15T14:30:00+00:00[Europe/London]", formatter)

// Handling daylight saving time transitions
val beforeDST = ZonedDateTime.of(2024, 3, 10, 1, 30, 0, 0, ZoneId.of("America/New_York"))
val afterDST = beforeDST.plusHours(2)

println(s"London: $londonTime")
println(s"New York: $newYorkTime")
println(s"London converted to NY: $londonToNewYork")
println(s"DST transition: $beforeDST -> $afterDST")

Duration and Period Calculations

Duration represents time-based amounts (hours, minutes, seconds), while Period represents date-based amounts (years, months, days). Choose the appropriate class based on your calculation requirements.

import java.time.{Duration, Period, LocalDate, LocalDateTime, Instant}
import java.time.temporal.ChronoUnit

// Duration for time-based calculations
val start = LocalDateTime.of(2024, 3, 15, 9, 0)
val end = LocalDateTime.of(2024, 3, 15, 17, 30)
val workDuration = Duration.between(start, end)

println(s"Work hours: ${workDuration.toHours} hours, ${workDuration.toMinutesPart} minutes")

// Period for date-based calculations
val birthDate = LocalDate.of(1990, 5, 20)
val currentDate = LocalDate.of(2024, 3, 15)
val age = Period.between(birthDate, currentDate)

println(s"Age: ${age.getYears} years, ${age.getMonths} months, ${age.getDays} days")

// Measuring execution time
val startTime = Instant.now()
// Simulate work
Thread.sleep(1000)
val endTime = Instant.now()
val executionDuration = Duration.between(startTime, endTime)

println(s"Execution time: ${executionDuration.toMillis} ms")

// ChronoUnit for specific unit calculations
val daysBetween = ChronoUnit.DAYS.between(birthDate, currentDate)
val hoursBetween = ChronoUnit.HOURS.between(start, end)

println(s"Days lived: $daysBetween")

Formatting and Parsing Dates

Custom formatters enable consistent date-time representation across your application. Pre-defined formatters handle common ISO standards, while custom patterns address specific requirements.

import java.time.LocalDateTime
import java.time.format.{DateTimeFormatter, FormatStyle}
import java.util.Locale

val dateTime = LocalDateTime.of(2024, 3, 15, 14, 30, 45)

// Predefined formatters
val isoFormat = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
val basicFormat = dateTime.format(DateTimeFormatter.BASIC_ISO_DATE)

// Custom patterns
val customFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
val customFormat = dateTime.format(customFormatter)

// Localized formatting
val usFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
  .withLocale(Locale.US)
val frenchFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
  .withLocale(Locale.FRANCE)

println(s"ISO: $isoFormat")
println(s"Custom: $customFormat")
println(s"US: ${dateTime.format(usFormatter)}")
println(s"French: ${dateTime.format(frenchFormatter)}")

// Parsing with error handling
def parseDate(dateString: String): Either[String, LocalDateTime] = {
  try {
    Right(LocalDateTime.parse(dateString, customFormatter))
  } catch {
    case e: Exception => Left(s"Failed to parse: ${e.getMessage}")
  }
}

println(parseDate("15/03/2024 14:30:45"))
println(parseDate("invalid-date"))

Practical Application: Business Day Calculator

This example demonstrates calculating business days between dates, excluding weekends and holidays—a common requirement in financial and scheduling applications.

import java.time.{LocalDate, DayOfWeek}
import java.time.temporal.ChronoUnit
import scala.annotation.tailrec

object BusinessDayCalculator {
  
  def isWeekend(date: LocalDate): Boolean = {
    val dayOfWeek = date.getDayOfWeek
    dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY
  }
  
  def isHoliday(date: LocalDate, holidays: Set[LocalDate]): Boolean = {
    holidays.contains(date)
  }
  
  def isBusinessDay(date: LocalDate, holidays: Set[LocalDate]): Boolean = {
    !isWeekend(date) && !isHoliday(date, holidays)
  }
  
  def businessDaysBetween(start: LocalDate, end: LocalDate, holidays: Set[LocalDate]): Long = {
    @tailrec
    def count(current: LocalDate, acc: Long): Long = {
      if (current.isAfter(end)) acc
      else {
        val newAcc = if (isBusinessDay(current, holidays)) acc + 1 else acc
        count(current.plusDays(1), newAcc)
      }
    }
    
    count(start, 0L)
  }
  
  def addBusinessDays(start: LocalDate, daysToAdd: Int, holidays: Set[LocalDate]): LocalDate = {
    @tailrec
    def add(current: LocalDate, remaining: Int): LocalDate = {
      if (remaining == 0) current
      else {
        val next = current.plusDays(1)
        val newRemaining = if (isBusinessDay(next, holidays)) remaining - 1 else remaining
        add(next, newRemaining)
      }
    }
    
    add(start, daysToAdd)
  }
}

// Usage example
val holidays = Set(
  LocalDate.of(2024, 1, 1),  // New Year
  LocalDate.of(2024, 12, 25) // Christmas
)

val startDate = LocalDate.of(2024, 3, 15)
val endDate = LocalDate.of(2024, 3, 25)

val businessDays = BusinessDayCalculator.businessDaysBetween(startDate, endDate, holidays)
val futureBusinessDay = BusinessDayCalculator.addBusinessDays(startDate, 5, holidays)

println(s"Business days between $startDate and $endDate: $businessDays")
println(s"5 business days after $startDate: $futureBusinessDay")

Working with Instant for Timestamps

Instant represents a point on the timeline in UTC, ideal for logging, event timestamps, and database storage where timezone interpretation happens at the presentation layer.

import java.time.{Instant, ZoneId, ZonedDateTime}
import java.time.temporal.ChronoUnit

// Creating instants
val now = Instant.now()
val epochStart = Instant.EPOCH
val specificInstant = Instant.ofEpochMilli(1710511800000L)

// Instant arithmetic
val oneHourLater = now.plus(1, ChronoUnit.HOURS)
val yesterday = now.minus(24, ChronoUnit.HOURS)

// Converting to zoned datetime for display
val zonedNow = now.atZone(ZoneId.of("America/New_York"))

// Comparing instants
val earlier = Instant.parse("2024-03-15T10:00:00Z")
val later = Instant.parse("2024-03-15T14:00:00Z")

println(s"Is earlier before later? ${earlier.isBefore(later)}")
println(s"Difference: ${Duration.between(earlier, later).toHours} hours")

// Database timestamp pattern
case class Event(id: String, timestamp: Instant, data: String)

val event = Event("evt-001", Instant.now(), "User logged in")
println(s"Event: ${event.id} at ${event.timestamp}")

Temporal Adjusters for Complex Date Logic

Temporal adjusters provide reusable date manipulation logic for common scenarios like finding the next business day, last day of month, or specific day of week.

import java.time.LocalDate
import java.time.temporal.{TemporalAdjusters, ChronoField}
import java.time.DayOfWeek

val date = LocalDate.of(2024, 3, 15)

// Built-in adjusters
val firstDayOfMonth = date.`with`(TemporalAdjusters.firstDayOfMonth())
val lastDayOfMonth = date.`with`(TemporalAdjusters.lastDayOfMonth())
val nextMonday = date.`with`(TemporalAdjusters.next(DayOfWeek.MONDAY))
val firstMondayOfMonth = date.`with`(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))

println(s"First day of month: $firstDayOfMonth")
println(s"Last day of month: $lastDayOfMonth")
println(s"Next Monday: $nextMonday")
println(s"First Monday: $firstMondayOfMonth")

// Custom adjuster
val nextBusinessDay = (temporal: java.time.temporal.Temporal) => {
  var result = LocalDate.from(temporal).plusDays(1)
  while (result.getDayOfWeek == DayOfWeek.SATURDAY || 
         result.getDayOfWeek == DayOfWeek.SUNDAY) {
    result = result.plusDays(1)
  }
  result
}

val nextBizDay = date.`with`(nextBusinessDay)
println(s"Next business day: $nextBizDay")

The java.time API provides comprehensive tools for date-time operations in Scala. Use immutable types, handle timezones explicitly, and leverage adjusters for complex logic. These patterns prevent common bugs in temporal calculations and ensure maintainable code across distributed systems.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.