MpLocalDate

A calendar date — no time, no timezone. The right type for birthdays, billing periods, and date pickers. Has the richest set of calendar operations of any type.

When to use MpLocalDate

  • Dates that are inherently timezone-independent (birthdays, holidays)
  • Date pickers and calendar UIs
  • Billing periods, subscription dates
  • Any "what day" question that doesn't need "what time"

Creating

import io.peekandpoke.ultra.datetime.MpLocalDate

val date = MpLocalDate.of(2025, 3, 15)
val withMonth = MpLocalDate.of(2025, Month.MARCH, 15)
val parsed = MpLocalDate.parse("2025-03-15")
val maybe = MpLocalDate.tryParse("not-a-date")  // null

// Boundaries
val genesis = MpLocalDate.Genesis    // -10000-01-01
val doomsday = MpLocalDate.Doomsday  // +10000-01-01

Properties

val date = MpLocalDate.of(2025, 3, 15)

date.year            // 2025
date.monthNumber     // 3
date.month           // Month.MARCH
date.day             // 15
date.dayOfWeek       // DayOfWeek.SATURDAY
date.dayOfYear       // 74
date.numDaysInMonth  // 31

Calendar arithmetic

Because MpLocalDate has no time component, calendar arithmetic is unambiguous — no DST to worry about:

val date = MpLocalDate.of(2025, 1, 31)

// Days and weeks
date.plusDays(1)       // 2025-02-01
date.minusDays(10)     // 2025-01-21
date.plusWeeks(2)      // 2025-02-14
date.minusWeeks(1)     // 2025-01-24

// Months — clamped to valid days
date.plusMonths(1)     // 2025-02-28 (not Feb 31!)
date.minusMonths(1)    // 2024-12-31

// Years
date.plusYears(1)      // 2026-01-31
date.minusYears(5)     // 2020-01-31

// Centuries
date.plusCenturies(1)  // 2125-01-31
Month-end clamping is important: January 31 plus one month is February 28 (or 29 in a leap year), not an error. The library does the right thing instead of throwing.

You can also use DateTimeUnit and periods:

import kotlinx.datetime.DateTimeUnit

date.plus(3, DateTimeUnit.MONTH)   // 2025-04-30
date.minus(1, DateTimeUnit.YEAR)   // 2024-01-31

val period = MpDatePeriod.of(years = 1, months = 2, days = 5)
date.plus(period)   // 2026-04-05
date.minus(period)  // 2023-11-26

Anchoring

Snap to calendar boundaries:

val date = MpLocalDate.of(2025, 7, 18)

// Month boundaries
date.atFirstOfMonth()            // 2025-07-01
date.atLastOfMonth()             // 2025-07-31
date.atNthOfMonth(15)            // 2025-07-15
date.atStartOfMonth()            // 2025-07-01 (alias)

// Year, quarter, half
date.atStartOfYear()             // 2025-01-01
date.atStartOfQuarterOfYear()    // 2025-07-01 (Q3 starts July)
date.atStartOfHalfOfYear()       // 2025-07-01 (H2 starts July)

// Decade and century
date.atStartOfDecade()           // 2020-01-01
date.atStartOfCentury()          // 2000-01-01

Week navigation

val date = MpLocalDate.of(2025, 3, 12)  // Wednesday

// Find specific days of the week
date.atStartOfNext(DayOfWeek.FRIDAY)      // 2025-03-14
date.atStartOfPrevious(DayOfWeek.MONDAY)  // 2025-03-10

// Get all Mondays in the month
val mondays = date.getDatesInMonth(listOf(DayOfWeek.MONDAY))
// [2025-03-03, 2025-03-10, 2025-03-17, 2025-03-24, 2025-03-31]

// Get specific day-of-month values
val paydays = date.getDatesInMonth(listOf(1, 15))
// [2025-03-01, 2025-03-15]

Converting to other types

A date alone is not enough to pinpoint a moment in time. The conversions reflect this:

val date = MpLocalDate.of(2025, 3, 15)

// To LocalDateTime — just adds midnight, no timezone needed
val startOfDay = date.toLocalDateTime()             // 2025-03-15T00:00
val atTime = date.atTime(MpLocalTime.of(14, 30))    // 2025-03-15T14:30

// To ZonedDateTime — REQUIRES a timezone
val zonedStart = date.atStartOfDay(MpTimezone.of("Europe/Berlin"))
val zonedNoon = date.atNoon(MpTimezone.of("Europe/Berlin"))
val zonedTime = date.atTime(
    MpLocalTime.of(14, 30),
    MpTimezone.of("Europe/Berlin"),
)

Creating ranges

val start = MpLocalDate.of(2025, 3, 10)
val end = MpLocalDate.of(2025, 3, 20)

// Open range [from, to)
val open = start.toRange(end)

// Closed range [from, to]
val closed = start.toClosedRange(end)

// Range from a period
val week = start.toRange(MpDatePeriod.of(days = 7))

// Entire month
val march = start.toEnclosingMonthRange()  // [March 1, March 31]

// Zoned range from a time slot
val slot = MpLocalTimeSlot(MpLocalTime.of(9, 0), MpLocalTime.of(17, 0))
val zonedRange = start.toRange(slot, MpTimezone.of("Europe/Berlin"))

Formatting

val date = MpLocalDate.of(2025, 3, 15)

date.toIsoString()            // "2025-03-15"
date.formatDdMmmYyyy()        // "15 Mar 2025"
date.format("dd/MM/yyyy")     // "15/03/2025"
date.format("yyyy-MM-dd")     // "2025-03-15"

JVM interop

// MpLocalDate -> java.time.LocalDate
val javaDate: java.time.LocalDate = mpDate.jvm

// java.time.LocalDate -> MpLocalDate
val mpDate: MpLocalDate = javaDate.mp