Verdandi
0.1.1indexedType-safe, immutable date and time DSL with English-like syntax, calendar-aware durations, composable intervals, recurrence rules, timezone-aware moments, expressive formatting and compile-time grammatical-number safety.
Type-safe, immutable date and time DSL with English-like syntax, calendar-aware durations, composable intervals, recurrence rules, timezone-aware moments, expressive formatting and compile-time grammatical-number safety.

A date & time DSL for KMP (Kotlin Multiplatform). Type-safe, immutable, reads like plain English.
now = Verdandi.now()
tomorrow = now adjust { add one day }
formatted = tomorrow format { yyyy/MM/dd..HH.mm }
interval = now..tomorrow
lastThirtyDays = Verdandi.interval { last thirty days from now }
weekdays = Verdandi.recurrence(now) { every weekdays until(birthday) }
relative = now.relativeTo(event) format {
onPast { }
onNow { }
onFuture { }
}
dependencies {
implementation("io.github.abraga:verdandi:0.1.1")
// Or for Compose projects:
implementation("io.github.abraga:verdandi-compose:0.1.1")
}
Every operation returns a new immutable instance. Grammatical number is enforced at compile time: one day compiles, one days does not.
now adjust { subtract two days }
now adjust {
add five hours
subtract thirtyFour minutes
}
// Alignment
now adjust { at startOf day }
now adjust { at endOf month }
now adjust {
weekStartsOn = Sunday
at startOf week
}
now adjust { atYear(); atMonth(); atDay() }
moment + 2.hours // kotlin.time.Duration
moment - 3.days
moment + 1.months // DateDuration (calendar-aware)
moment1..moment2 // VerdandiInterval
tomorrow.isTomorrow()
tomorrow.isToday()
tomorrow.isWeekend()
tomorrow isSameDayAs now
moment.wasYesterday()
moment1 isBefore moment2
moment1 isAfter moment2
moment.isBetween(start, end)
val relative = now.relativeTo(adjusted) format {
onPast { "$it ago" }
onNow { "right now" }
onFuture { }
}
println(relative)
short = now.relativeTo(adjusted) format {
maxUnits =
onPast { }
onNow { }
onFuture { }
}
println(short)
The underlying instant stays the same — only decomposed components change.
val utc = Verdandi.from("2026-06-15T12:00:00Z")
val tokyo = utc inTimeZone VerdandiTimeZone.of("Asia/Tokyo")
utc.inMilliseconds == tokyo.inMilliseconds // true
tokyo.component.hour // 21
A VerdandiInterval is a half-open range [start, end).
val interval = today..tomorrow
Verdandi.interval { last thirty days }
Verdandi.interval { next two weeks }
interval.contains(moment)
interval.overlaps(other)
interval.intersection(other)
interval.union(other)
interval.duration()
expanded = interval adjust { expandBoth(days) }
aligned = interval adjust {
shiftBoth(hours)
shiftEnd(minutes)
alignToFullDays()
}
VerdandiRecurrenceMoments implements List<VerdandiMoment>.
Verdandi.recurrence(today) {
every weekdays at { hours } until deadline
}
Verdandi.recurrence(start, limit = ) { every day indefinitely }
Verdandi.recurrence(lastMonth) {
every day on fridays until(deadline)
} filter { it.component.day.value == } format { MM-dd }
recurrence.matches(someMoment)
recurrence.exclude(holiday)
recurrence.exclude(holiday1, holiday2, ...)
val component = moment.component
component.year.value // 2026
component.month.value // 6
component.day.value // 15
component.dayOfWeek.value // 7 (ISO: 1 = Mon, 7 = Sun)
component.quarter.value // 2
[!TIP] Components are lazily computed and cached on first access.
Verdandi.config.configure {
defaultTimeZone = VerdandiTimeZone.UTC
defaultWeekStart = Sunday
}
[!IMPORTANT] Call once at application startup. Thread-safe, but affects all subsequent calls.
VerdandiMoment, VerdandiInterval, and VerdandiRecurrenceMoments support kotlinx.serialization.
{ "epoch": 1750000000000, "timeZoneId": "America/Sao_Paulo" }
verdandi/
├── api/ # Public API — stable, versioned
└── internal/ # Implementation — may change between releases
Immutable operations, sealed/inline value classes for type safety, @VerdandiDslMarker for DSL isolation, and expect/actual for platform-specific timezone resolution.
Apache 2.0 — see LICENSE for details.
A VerdandiMoment wraps epoch milliseconds with optional timezone context.
Verdandi.now() // current instant
Verdandi() // shorthand
Verdandi.at(2026, 6, 15, 14, 30) // from components
Verdandi.from(1750000000000L) // from epoch ms
Verdandi.fromSeconds(1750000000L) // from epoch seconds
Verdandi.from("2026-06-15T14:30:00Z") // from ISO-8601
Verdandi.parse("15/06/2026 14:30", "dd/MM/yyyy HH:mm") // custom pattern
moment format { yyyy/MM/dd T HH.mm } // "2026/02/11 21:24"
moment format("yy-MM-dd") // "26-02-13"
moment format "EEEE, MMMM dd, yyyy" // "Sunday, June 15, 2026"
| Directive | Description | Example |
|---|---|---|
yyyy / yy | Year (4 / 2 digits) | 2026 / 26 |
MMMM / MMM / MM | Month (full / abbr / padded) | January / Jan / 01 |
dd / d | Day (padded / variable) | 09 / 9 |
EEEE / EEE / E | Weekday (full / abbr / digit) | Monday / Mon / 1 |
HH / hh | Hour (0-23 / 1-12) | 14 / 02 |
mm / ss / SSS | Minute / Second / Millisecond | 05 / 09 / 456 |
A | AM/PM | PM |
Q | Quarter | 2 |
Z | UTC offset | +09:00 / Z |
Escape literals with single quotes: 'at' → at
// Live clock
val now by rememberCurrentMoment(refreshInterval = 1.seconds)
Text(now format { HH.mm.ss })
// Stable across recompositions
val snapshot = rememberMoment()
val birthday = rememberMoment(year = 1990, month = 5, day = 20)
// Survives configuration changes
val saved = rememberSavableMoment()
// Two-way binding
val (moment, setMoment) = rememberMutableMomentState()
Button(
onClick = {
setMoment(moment adjust { add one day })
}
) {
Text("Next day")
}
// Re-evaluated when source changes
val startOfDay = rememberAdjustedMoment(selectedDate) { at startOf day }
// Intervals
val interval = rememberInterval(startMoment, endMoment)
val saveable = rememberSavableInterval(startMoment, endMoment)
// Composition local
CompositionLocalProvider(LocalVerdandiMoment provides live) {
Text(LocalVerdandiMoment.current.toString())
}
Surfaced from shared tags and platforms — no rankings paid for.