Ranges & Time Slots
Time intervals at every level — from calendar date ranges to intra-day time slots — with containment checks, intersection tests, and set operations.
All range types (except MpLocalTimeSlot) have a Partial variant for when only one boundary is
known.
This is common in search filters, open-ended subscriptions, and "from now on" queries.
Convert to a concrete range via asValidRange(), which fills in Genesis or Doomsday for the missing
boundary.
MpLocalDateRange
An open-ended date range [from, to) — the end date is excluded. This is the natural model
for hotel stays, billing periods, and date iteration:
import io.peekandpoke.ultra.datetime.MpLocalDateRange
import io.peekandpoke.ultra.datetime.MpLocalDate
val range = MpLocalDateRange(
from = MpLocalDate.of(2025, 3, 10),
to = MpLocalDate.of(2025, 3, 15),
)
println(range.numberOfDays) // 5
println(range.numberOfNights) // 5
// Iterate over all dates in the range
val dates = range.asListOfDates()
// [2025-03-10, 2025-03-11, 2025-03-12, 2025-03-13, 2025-03-14]
// Properties
range.asDatePeriod // MpDatePeriod(0y, 0m, 5d)
range.asClosedRange // MpClosedLocalDateRange [2025-03-10, 2025-03-14]
range.hasStart // true (from is after Genesis)
range.hasEnd // true (to is before Doomsday)
range.isOpen // false (both boundaries set)
range.isValid // true (from < to)
// Factory methods
val forever = MpLocalDateRange.forever // Genesis to Doomsday
val from = MpLocalDateRange.beginningAt(date) // date to Doomsday
val until = MpLocalDateRange.endingAt(date) // Genesis to date
// Convert to timezone-aware range
val zoned = range.toZonedTimeRange(MpTimezone.of("Europe/Berlin"))
.fromNoonToNoon // MpZonedDateTimeRange
// Convert to Partial
val partial = range.asPartialRange() // Partial(from, to) from=10, to=13 — you check out on the 13th, not stay through it.
Set operations
val a = MpLocalDateRange(
from = MpLocalDate.parse("2024-01-01"),
to = MpLocalDate.parse("2024-06-30"),
)
val b = MpLocalDateRange(
from = MpLocalDate.parse("2024-03-01"),
to = MpLocalDate.parse("2024-09-30"),
)
// Containment
a.contains(MpLocalDate.parse("2024-03-15")) // true
a.contains(b) // false (b extends past a)
// Intersection and touching
a.intersects(b) // true
a.touches(b) // true
a.isAdjacentTo(b) // false (they overlap)
// Merge overlapping ranges
a.mergeWith(b) // [MpLocalDateRange(2024-01-01, 2024-09-30)]
// Cut — subtract one range from another
a.cutAway(b) // [MpLocalDateRange(2024-01-01, 2024-03-01)]
// Also works with closed ranges
a.intersects(closedRange) MpLocalDateRange.Partial
// Only a start — open-ended into the future
val fromMarch = MpLocalDateRange.Partial(
from = MpLocalDate.of(2025, 3, 1),
to = null,
)
// Only an end — open-ended into the past
val untilJune = MpLocalDateRange.Partial(
from = null,
to = MpLocalDate.of(2025, 6, 1),
)
// Both null — represents "any time"
val anytime = MpLocalDateRange.Partial(from = null, to = null)
// Convert to a concrete range (fills in Genesis/Doomsday)
val concrete = fromMarch.asValidRange()
// MpLocalDateRange(2025-03-01, +10000-01-01) MpClosedLocalDateRange
A closed date range [from, to] — the end date is included:
import io.peekandpoke.ultra.datetime.MpClosedLocalDateRange
val closed = MpClosedLocalDateRange(
from = MpLocalDate.of(2025, 3, 10),
to = MpLocalDate.of(2025, 3, 14),
)
println(closed.numberOfDays) // 5 (includes both endpoints)
println(closed.numberOfNights) // 4
// Properties
closed.asOpenRange // MpLocalDateRange [2025-03-10, 2025-03-15)
closed.asDatePeriod // MpDatePeriod(0y, 0m, 5d)
closed.hasStart // true
closed.hasEnd // true
closed.isOpen // false
closed.isValid // true
// Factory methods
val forever = MpClosedLocalDateRange.forever
val from = MpClosedLocalDateRange.beginningAt(date) // date to Doomsday
val until = MpClosedLocalDateRange.endingAt(date) // Genesis to date
// Convert to timezone-aware range
val zoned = closed.toZonedTimeRange(MpTimezone.of("Europe/Berlin"))
// Iterate dates
closed.asListOfDates() // [2025-03-10, ..., 2025-03-14]
// Set operations (same as MpLocalDateRange)
closed.contains(date)
closed.contains(otherClosed)
closed.intersects(otherClosed)
closed.intersects(openRange) // works with open ranges too
closed.touches(otherClosed)
closed.isAdjacentTo(otherClosed)
closed.mergeWith(otherClosed)
closed.cutAway(otherClosed) MpClosedLocalDateRange.Partial
val partial = MpClosedLocalDateRange.Partial(
from = MpLocalDate.of(2025, 3, 10),
to = null,
)
val concrete = partial.asValidRange()
// MpClosedLocalDateRange(2025-03-10, +10000-01-01) MpInstantRange
A range of absolute timestamps. Supports containment checks, intersection tests, set subtraction, and timezone-aware arithmetic:
import io.peekandpoke.ultra.datetime.MpInstantRange
import io.peekandpoke.ultra.datetime.MpInstant
val start = MpInstant.parse("2025-03-15T08:00:00Z")
val end = MpInstant.parse("2025-03-15T17:00:00Z")
val workday = MpInstantRange(from = start, to = end)
// Properties
workday.duration // 9 hours (kotlin.time.Duration)
workday.hasStart // true (from is after Genesis)
workday.hasEnd // true (to is before Doomsday)
workday.isOpen // false
workday.isValid // true (from < to)
// Factory methods
val fromDuration = MpInstantRange.of(start, 2.hours) // start + 2h
val forever = MpInstantRange.forever
val from = MpInstantRange.beginningAt(start) // start to Doomsday
val until = MpInstantRange.endingAt(end) // Genesis to end
// Convert to zoned
val zoned = workday.atZone(MpTimezone.of("Europe/Berlin"))
val utc = workday.atZone(MpTimezone.UTC)
val local = workday.atSystemDefaultZone()
// Convert to Partial
val partial = workday.asPartialRange() Set operations
// Containment — is a specific instant inside this range?
val noon = MpInstant.parse("2025-03-15T12:00:00Z")
workday.contains(noon) // true
// Also works with MpZonedDateTime and other MpAbsoluteDateTime types
workday.contains(zonedDateTime) // true/false
// Range containment
workday.contains(otherRange) // true if other is fully inside
// Intersection
val afternoon = MpInstantRange(
from = MpInstant.parse("2025-03-15T13:00:00Z"),
to = MpInstant.parse("2025-03-15T20:00:00Z"),
)
workday.intersects(afternoon) // true
workday.touches(afternoon) // true
workday.isAdjacentTo(afternoon) // false (they overlap)
// Merge
workday.mergeWith(afternoon) // one merged range
// Cut — subtract one range from another
workday.cutAway(afternoon) // [08:00-13:00] Arithmetic
import kotlin.time.Duration.Companion.hours
import kotlinx.datetime.DateTimeUnit
// Shift by absolute Duration
val shifted = workday.plus(2.hours)
val earlier = workday.minus(1.hours)
// Shift by calendar unit (DST-aware, requires timezone)
val tomorrow = workday.plus(1, DateTimeUnit.DAY, MpTimezone.of("Europe/Berlin").kotlinx)
val lastWeek = workday.minus(7, DateTimeUnit.DAY, MpTimezone.of("Europe/Berlin").kotlinx) MpInstantRange.Partial
val partial = MpInstantRange.Partial(
from = MpInstant.parse("2025-03-15T08:00:00Z"),
to = null, // open-ended
)
val concrete = partial.asValidRange()
// MpInstantRange(2025-03-15T08:00:00Z, +10000-01-01T00:00:00Z)
// Convert partial to a date range in a specific timezone
val dateRange = partial.asDateRange(MpTimezone.of("Europe/Berlin"))
// MpClosedLocalDateRange.Partial MpZonedDateTimeRange
A timezone-aware datetime range. Knows its duration, supports set operations, and can be shifted with DST-aware arithmetic:
import io.peekandpoke.ultra.datetime.MpZonedDateTimeRange
import io.peekandpoke.ultra.datetime.MpTimezone
val berlin = MpTimezone.of("Europe/Berlin")
val range = MpZonedDateTimeRange(
from = MpInstant.parse("2025-03-15T08:00:00Z").atZone(berlin),
to = MpInstant.parse("2025-03-15T17:00:00Z").atZone(berlin),
)
// Properties
range.duration // 9 hours
range.hasStart // true
range.hasEnd // true
range.isOpen // false — both boundaries present
range.isValid // true — from is before to
// Factory methods
val fromDuration = MpZonedDateTimeRange.of(zonedStart, 2.hours)
val fromPeriod = MpZonedDateTimeRange.of(zonedStart, MpDateTimePeriod.of(months = 1))
val forever = MpZonedDateTimeRange.forever
val from = MpZonedDateTimeRange.beginningAt(zonedStart)
val until = MpZonedDateTimeRange.endingAt(zonedEnd)
// Conversions
range.asDateRange() // MpClosedLocalDateRange
range.toInstantRange() // MpInstantRange (drops timezone)
range.atZone(otherTimezone) // re-zone to different timezone
range.asPartialRange() // Partial(from, to) Set operations
// Containment
range.contains(zonedDateTime) // MpAbsoluteDateTime
range.contains(localDateTime, timezone) // MpLocalDateTime + timezone
range.contains(otherRange) // full range containment
// Intersection and touching
range.intersects(otherRange)
range.touches(otherRange)
range.isAdjacentTo(otherRange)
// Merge and cut
range.mergeWith(otherRange)
range.cutAway(otherRange) Arithmetic
// Shift by absolute Duration
val shifted = range.plus(2.hours)
val earlier = range.minus(1.hours)
// Shift by calendar unit (DST-aware — uses the range's own timezone)
val tomorrow = range.plus(1, DateTimeUnit.DAY)
val nextMonth = range.plus(1, DateTimeUnit.MONTH)
val lastWeek = range.minus(7, DateTimeUnit.DAY) MpZonedDateTimeRange.Partial
val partial = MpZonedDateTimeRange.Partial(
from = MpInstant.parse("2025-03-15T08:00:00Z").atZone(berlin),
to = null, // open-ended
)
val concrete = partial.asValidRange()
// Convert to a date range partial
val datePartial = partial.asDateRange() // MpClosedLocalDateRange.Partial MpLocalTimeSlot
A time range within a single day. Purpose-built for scheduling, availability calendars, and business hours:
import io.peekandpoke.ultra.datetime.MpLocalTimeSlot
import io.peekandpoke.ultra.datetime.MpLocalTime
val morning = MpLocalTimeSlot(
from = MpLocalTime.of(9, 0),
to = MpLocalTime.of(12, 0),
)
// Properties
morning.duration // 3 hours
morning.isValid // true (duration > 0)
// Factory methods
val fromDuration = MpLocalTimeSlot.of(MpLocalTime.of(9, 0), 3.hours)
val fromSeconds = MpLocalTimeSlot.ofSecondsOfDay(32400, 43200) // 9:00-12:00
val wholeDay = MpLocalTimeSlot.completeDay // 00:00 to 23:59:59.999
// Formatting
morning.formatHhMm() // "09:00 - 12:00"
morning.formatHhMmSs() // "09:00:00 - 12:00:00" Set operations
val afternoon = MpLocalTimeSlot(
from = MpLocalTime.of(13, 0),
to = MpLocalTime.of(17, 0),
)
// Containment
morning.contains(MpLocalTime.of(10, 30)) // true
morning.contains(afternoon) // false
// Intersection and touching
morning.intersects(afternoon) // false (gap between them)
morning.touches(afternoon) // false (12:00 to 13:00 gap)
val extended = MpLocalTimeSlot(
from = MpLocalTime.of(11, 0),
to = MpLocalTime.of(14, 0),
)
morning.intersects(extended) // true
morning.touches(extended) // true
morning.isAdjacentTo(extended) // false (they overlap)
// Merge overlapping slots
morning.mergeWith(extended)
// [MpLocalTimeSlot(09:00, 14:00)]
// Cut — remove one slot from another
afternoon.cutAway(extended)
// [MpLocalTimeSlot(14:00, 17:00)]
// Split into fixed-duration slots with gaps
morning.splitWithGaps(duration = 1.hours, gap = 15.minutes)
// [09:00-10:00, 10:15-11:15] Range operations summary
| Operation | DateRange | ClosedDateRange | InstantRange | ZonedRange | TimeSlot |
|---|---|---|---|---|---|
contains(point) | yes | yes | yes | yes | yes |
contains(range) | yes | yes | yes | yes | yes |
intersects() | yes | yes | yes | yes | yes |
touches() | yes | yes | yes | yes | yes |
isAdjacentTo() | yes | yes | yes | yes | yes |
mergeWith() | yes | yes | yes | yes | yes |
cutAway() | yes | yes | yes | yes | yes |
splitWithGaps() | - | - | - | - | yes |
asListOfDates() | yes | yes | - | - | - |
duration | - | - | yes | yes | yes |
plus() / minus() | - | - | yes | yes | - |
| Partial variant | yes | yes | yes | yes | - |