Description
Originally suggested in PR #173 which contained both the sealed hierarchy and YearMonth. The YearMonth feature request is in #168. The implementation of YearMonth (and Year) is related to this proposal, but maybe for the discussion here we should focus on the sealed hierarchy aspect.
This is just a very quick example of how we could add support for an arbitrary precision date that can be a Year or YearMonth or LocalDate or ZonedDateTime (which could itself be a sealed class of OffsetDateTime and RegionDateTime - see #175).
At least in the medical space it's pretty common to have flexible date precisions in the official specifications, so you have to support at least these operations
- parsing
- machine-formatting back to the same precision string (so parsing will result in the same object)
- human-formatting (displaying in the UI, with different precisions levels)
- sorting/comparing in a natural way - at least how humans would sort them
You might also want to treat the data differently based on its type. Here it helps a lot to have a sealed interface, so you can easily match all possible cases (which is also useful for implementing the formatting and sorting etc. functions).
Using a sealed hierarchy would allow writing code in a more statically safe way because you can enforce with types that at least a certain precision level is provided. You can also exhaustively match on all subtypes, which makes working with arbitrary precision dates much more comfortable.
The latest example code is here: https://github.com/Kotlin/kotlinx-datetime/compare/master...wkornewald:feature/yearmonth-and-arbitraryprecisiondate?expand=1
To start the discussion, here's a copy of the code, which is a strict precision hierarchy ranging from Year to ZonedDateTime and it only contains the types that humans usually work with:
public sealed interface AtLeastYear : Comparable<AtLeastYear> {
public val year: Int
override fun compareTo(other: AtLeastYear): Int {
val result = toComparisonInstant().compareTo(other.toComparisonInstant())
return if (result == 0) hierarchyLevel.compareTo(other.hierarchyLevel) else result
}
public fun toComparisonInstant(): Instant
public companion object {
public fun parse(value: String): AtLeastYear {
TODO()
}
}
}
public val AtLeastYear.hierarchyLevel: Int get() = when (this) {
is Year -> 0
is YearMonth -> 1
// is LocalDate -> 2
// is ZonedDateTime -> 3
}
public data class Year(override val year: Int) : AtLeastYear {
override fun toComparisonInstant(): Instant =
"${year.formatLen(4)}-01-01T00:00:00Z".toInstant()
}
public sealed interface AtLeastYearMonth : AtLeastYear {
public val month: Month
public companion object {
public fun parse(value: String): AtLeastYearMonth {
TODO()
}
}
}
public data class YearMonth(override val year: Int, override val month: Month) : AtLeastYearMonth {
override fun toComparisonInstant(): Instant =
"${year.formatLen(4)}-${month.number.formatLen(2)}-01T00:00:00Z".toInstant()
}
public sealed interface AtLeastDate : AtLeastYearMonth {
// ...
}
// class LocalDate : AtLeastDate
// sealed class ZonedDateTime : AtLeastDate
Of course one could also implement this outside of kotlinx.datetime, but having it here in this lib makes more sense because it works out of the box without any indirections, wrapping or custom types and it's helpful for everyone who must tackle this kind of problem.