kotlinx-datetime
                                
                                 kotlinx-datetime copied to clipboard
                                
                                    kotlinx-datetime copied to clipboard
                            
                            
                            
                        Add an OffsetDateTime type
it would be quite good if there was a type (maybe let's call it OffsetDateTime to distinguish it from a possible ZonedDateTime) that can represent a LocalDateTime and ~~TimeZone~~ ZoneOffset together. This is especially useful for parsing RFC3339 compatible strings in common code (e.g. https://github.com/OpenAPITools/openapi-generator/pull/7353#discussion_r551024354).
It can easily be accomplished without any platform-specific code, I've already drafted a small example with should be enough for most use-cases (but uses some custom parsing logic, which will only work for RFC3339 compatible strings)
Example
package com.example
import kotlinx.datetime.*
import kotlin.jvm.JvmStatic
/**
 * Represents a [LocalDateTime] and the respective [ZoneOffset] of it.
 */
public class OffsetDateTime private constructor(public val dateTime: LocalDateTime, public val offset: ZoneOffset) {
    override fun toString(): String {
        return if (offset.totalSeconds == 0) {
            "${dateTime}Z"
        } else {
            "$dateTime$offset"
        }
    }
    /**
     * Converts the [OffsetDateTime] to an [Instant]. This looses the [ZoneOffset] information, because the date and time
     * is converted to UTC in the process.
     */
    public fun toInstant(): Instant = dateTime.toInstant(offset)
    /**
     * Returns a new [OffsetDateTime] with the given [TimeZone].
     */
    public fun atZoneSameInstant(newTimeZone: TimeZone): OffsetDateTime {
        val instant = dateTime.toInstant(offset)
        val newDateTime = instant.toLocalDateTime(newTimeZone)
        return OffsetDateTime(newDateTime, newTimeZone.offsetAt(instant))
    }
    public companion object {
        private val zoneRegex by lazy {
            Regex("""[+\-][0-9]{2}:[0-9]{2}""")
        }
        /**
         * Parses an [OffsetDateTime] from a RFC3339 compatible string.
         */
        @JvmStatic
        public fun parse(string: String): OffsetDateTime = when {
            string.contains('Z') -> OffsetDateTime(
                LocalDateTime.parse(string.substringBefore('Z')),
                TimeZone.UTC.offsetAt(Instant.fromEpochMilliseconds(0)),
            )
            string.contains('z') -> OffsetDateTime(
                LocalDateTime.parse(string.substringBefore('z')),
                TimeZone.UTC.offsetAt(Instant.fromEpochMilliseconds(0)),
            )
            zoneRegex.matches(string) -> {
                val dateTime = LocalDateTime.parse(string.substring(0, string.length - 6))
                val tz = TimeZone.of(string.substring(string.length - 6))
                val instant = dateTime.toInstant(tz)
                val offset = tz.offsetAt(instant)
                OffsetDateTime(
                    dateTime,
                    offset,
                )
            }
            else -> throw IllegalArgumentException("Date \"$string\" is not RFC3339 compatible")
        }
        /**
         * Creates an [OffsetDateTime] from an [Instant] in a given [TimeZone] ([TimeZone.UTC] by default).
         */
        @JvmStatic
        public fun ofInstant(instant: Instant, offset: TimeZone = TimeZone.UTC): OffsetDateTime = OffsetDateTime(
            instant.toLocalDateTime(offset),
            offset.offsetAt(instant),
        )
        /**
         *
         */
        @JvmStatic
        public fun of(dateTime: LocalDateTime, offset: ZoneOffset): OffsetDateTime = OffsetDateTime(dateTime, offset)
    }
}
It would probably be useful to use an ZoneOffset for this, but it cannot be (directly) parsed currently
conversion from and to java.time is also easy (using the example):
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toJavaZoneOffset
import kotlinx.datetime.toKotlinLocalDateTime
import kotlinx.datetime.toKotlinZoneOffset
public fun java.time.OffsetDateTime.toKotlinOffsetDateTime(): OffsetDateTime {
    return OffsetDateTime.of(
        toLocalDateTime().toKotlinLocalDateTime(),
        toOffsetTime().offset.toKotlinZoneOffset()
    )
}
public fun OffsetDateTime.toJavaOffsetDateTime(): java.time.OffsetDateTime {
    return java.time.OffsetDateTime.of(dateTime.toJavaLocalDateTime(), offset.toJavaZoneOffset())
}
So did I get it right that you need such a type to represent deserialized values of the built-in date-time OpenAPI format?
Why is it important to preserve the offset information in this case? What can happen if the Instant type is used instead?
So not only specifically for OpenAPI, but in general to represent RFC3339 Datetimes (which is meant to be the Internet standard).
You can of course use an Instant here, but that means loosing the offset information. I don't have a specific use-case at hand right now, but I'm sure there was a reason RFC3339 was designed this way and I can think of some cases where the offset could be useful (for example if the server already converts it to the user local time as configured in the server or this information could also represent the local time of the server). I don't know if it's smart to use this, but as in this specific case it's often about existing APIs, this could render APIs unusable. You might find some more use-cases in the sources of the RFC.
Also as far as I'm aware Instant also can't parse RFC3339.
One specific use-case - our API deals with public transport departure times (bus, metro, etc) in various cities. Public transport departure times (like flights) are always displayed in the local city time. The API returns times conforming to RFC3339 such as 2020-07-31T09:16:15+02:00. If these are parsed and represented as an Instant, we need an additional redundant data point - the zone offset - to convert to and display LocalDateTimes to the user.
This does not imply that it is necessary to have a kotlinx.datetime.OffsetDateTime, but at the very least it would be useful to have a parsing function that returns a LocalDateTime and ZoneOffset from an RFC3339 time.
FWIW: Crystal's equivalent of Instant always carries a location property to designate the time zone instance. Internal represenatation is normalized to UTC, so for example comparison between instances with different locations doesn't require conversion. The time zone information is only used for interaction with a human-readable datetime representation (parsing, stringfigication, calendrical arithmetics).
This has proven to be very practical and enables use cases like the one described here without adding too much complexity, and actually simplifying the API because you don't need to pass a TimeZone instance to every method which operates time-zone aware.
Came here searching for a multiplatform replacement for java.time.ZonedDateTime. It's really surprising that kotlinx-datetime isn't equally able to parse an ISO-8601 and/or a RFC-3339 date string that includes a non-zulu time-zone. Or am I getting it wrong?
Fixed in recent release: https://github.com/Kotlin/kotlinx-datetime/releases/tag/v0.2.0
Sorry, I mistaken this for another issue.
Fixed in recent release: https://github.com/Kotlin/kotlinx-datetime/releases/tag/v0.2.0
But parsing it to an instant the time zone is getting lost isn't it?
The parsed time zone is not returned to the user, yes.
Which does result in the loss of information
Then unfortunately this is not a solution / fix for us, as we have to keep the time zone.
We do want to support returning the parsed offsets, but the exact implementation is unclear: maybe it's sufficient to just return a Pair, maybe something heavier, like ZonedDateTime, needs to be introduced, or something in-between would be the best choice.
To decide this, we need to consider the specific use cases. @harry248, could you please share yours? @justasm already mentioned that an API they're dealing with returns the instants with a time zone that also indicates how the instant should be displayed to the end-user—this is a good point, thanks!
@dkhalanskyjb It's exactly the same scenario in our case.
My use-case for OffsetDateTime is to parse data from a database. So serialization and deserialization are sufficient. But Ideally we can override them automatically thorugh kotlinx
I've had one (rare & weak) use case for OffsetDateTime so far, just recently:
I had to parse an ISO 8601 timestamp from a 3rd party API. While I could parse it as Instant I also needed to know the offset in that case because that API doesn't provide a proper time zone. However I've asked them to include it as that would make more sense and be more reliable than using offsets.
That use case is more or less https://github.com/Kotlin/kotlinx-datetime/issues/90#issuecomment-758736441 where also the API isn't providing an ideal data format. It should probably provide a local datetime + time zone instead of datetime merged with an offset.
In general over many different projects there hasn't been any instance where I've needed ZonedDateTime.
In most cases where a TimeZone is needed it's stored and passed around separately anyway.
For example for each insurance policy we store a TimeZone that's used for all LocalDate(Time) instances affecting that insurance. No need for ZonedDateTime here. TimeZone is typically part of a context (e.g. the insurance policy, a calendar entry, a geographical location) and thus not linked directly to individual Date(Time) instances. That was true for all projects so far.
An offset I've never needed except for the very rare use case mentioned initially.
Our use-case is to be able to losslessly serialize/deserialize and work with arbitrary future datetimes with zone and DST information like java.time.ZonedDateTime already does:
- offset only (e.g. +02:00)
- zone id only (e.g. Europe/Berlin)
- zone id with a flag for DST vs. standard (e.g. [Europe/Berlin][DST],[Europe/Berlin][ST]or maybe+02:00[Europe/Berlin])
Only (3) can correctly represent a future event/datetime if the zone's offset and DST handling gets changed unexpectedly. Using separate LocalDateTime and TimeZone and dst flags is unnecessarily complicated and error-prone.
Finally, with such a flexible ZonedDateTime maybe there is no need for a separate OffsetDateTime (I never had a need for that at least).
Hi @wkornewald!
Only (3) can correctly represent a future event/datetime if the zone's offset and DST handling gets changed unexpectedly.
Could you elaborate on this? I don't see why this is true. Consider two cases here:
- The instant is what's important. For example, we may need to run something exactly 2000 hours later. In this case, this information is encoded as an Instant, and optionally aTimeZoneto convert toLocalDateTimefor human readability.
- The clock readings is what's important. For example, something, like a meeting or a train departure, needs to happen on June 12th, 13:30. In this case, the right thing to do is to transfer the LocalDateTime+TimeZoneonly: it doesn't matter if it's DST or not, what offset it is, etc., the only thing that matters is that the date (which is the ground truth in this case) map correctly to theInstantat that instant.
Am I missing some case here where LocalDateTime + TimeZone + ZoneOffset are all important?
In general, IIRC, this is the first time someone asks that we handle the DST in a specific manner.
Note that point (3) is just an edge case to be able to represent a precise umanbiguous clock readings time. My main suggestion is to have a ZonedDateTime that at least fulfills (1) and (2), but additionally being able to losslessly represent serialized string-based dates produced by java.time would be nice.
Regarding (3): If you want to trigger an event at an exact point in clock readings time then you have two different interpretations. How do you distinguish the point in time before the DST change vs. the time after the change? 02:30 could mean two points in time.
How do you distinguish the point in time before the DST change vs. the time after the change? 02:30 could mean two points in time.
Tricky.
- If we assume that the DST dates do not suddenly change, we can use Instantto represent this: the translation fromInstanttoLocalDateTimewon't be ambiguous.
- If we assume that the DST dates may suddenly change and the datetime is important, then we want to adapt to the changes. How do we do that?
- If some datetime used to be ambiguous, but after the rules are changed, it is no longer ambiguous, then the DSTflag should be ignored. Seems fairly obvious: since the datetime is important, we don't want to do anything that won't preserve it.
- If some datetime used to be unambiguous, but after the rules are changed, it becomes ambiguous, then, yes, we could use the DST flag to choose the correct side of the overlap, but would this do any good? My worry is that this choice would be rather arbitrary. If someone used, say, a date picker to choose 12:15on a particular day and it happened to be DST, but then the rules changed so that12:15occurs twice on that day, does the existing DST flag really say anything about what the user would have wanted in this case? I don't think so.
- If some datetime used to be ambiguous (for example, 12:15 when the clocks are moved from 13:00 to 12:00), and after the rules are changed, it stays ambiguous (for example, the clocks are moved from 12:30 to 11:30), then, yes, the DST flag would help. Now, this (to me) seems really obscure and not worthy of a special case. I don't really know if such an event ever happened even, and wouldn't be surprised if it hasn't.
 
- If some datetime used to be ambiguous, but after the rules are changed, it is no longer ambiguous, then the 
Imagine a „simple“ calendar app that wants to allow users to specify the DST flag when an event occurs at some ambiguous time. The exact day or time when the switch to DST occurs might change in the future and an Instant can’t deal with that, so the only option would be ZonedDateTime with a nullable DST Boolean (part of TimeZone?).
But you're right, this is pretty obscure and indeed maybe not worth it - though then we can't be fully compatible with everything representable by java.time then. Anyway, my main request was having a ZonedDateTime where the zone can be either an id or an offset and I just continued to think of obscure edge cases and then the DST question came up, so I compared how java.time handles that. We're discussing the DST point too much and let's better focus on the core ZonedDateTime aspect. 😄
Imagine a „simple“ calendar app that wants to allow users to specify the DST flag
Can't really imagine that. I'd expect the calendar app to provide a time picker, which, if the author of the calendar so chose, would have some times repeated.
we can't be fully compatible with everything representable by java.time then
Oh, I think it's not a viable goal at all, given how vast the Java Time API is. We want this library to be simple and discoverable, not all-encompassing.
let's better focus on the core ZonedDateTime aspect
Ok, sure. In your case, since, it looks like, you can choose the format in which to send data, right? Then, I think a good choice depending on the circumstance is either an Instant for representing specific moments (+ an optional TimeZone if the way the datetimes are displayed should also be controlled) or LocalDateTime + TimeZone for representing "the moment when the wall clock in the given time zone displays this". What do you think?
I compared how java.time handles that. We're discussing the DST point too much and let's better focus on the core ZonedDateTime aspect.
I think aligning with java's behavior makes a lot of sense instead of rolling your own and dealing with a lot of inconsistent behavior between different services.
LocalDateTime+TimeZonefor representing "the moment when the wall clock in the given time zone displays this".
This seems to basically be how Java implements it and is also how I work around it right now.
let's better focus on the core ZonedDateTime aspect
Ok, sure. In your case, since, it looks like, you can choose the format in which to send data, right? Then, I think a good choice depending on the circumstance is either an
Instantfor representing specific moments (+ an optionalTimeZoneif the way the datetimes are displayed should also be controlled) orLocalDateTime+TimeZonefor representing "the moment when the wall clock in the given time zone displays this". What do you think?
I can't fully choose the format because we have an existing system and already pre-existing official specifications that we have to support.
Actively juggling with LocalDateTime and TimeZone as separate values is definitely not enough.
- How do you compare dates?
- How do you parse and format dates?
- How do you serialize/deserialize? This is almost always a single field in the serialized data (e.g. JSON).
- How do you make sure no mistakes happen because people forget to take the TimeZone into account in the whole codebase with tons of developers of varying experience?
We had to build our own ZonedDateTime to deal with this and I'm pretty sure everyone else is doing the same thing because this is a very common use-case. Other libraries already provide it out of the box and I think this library should do it, too.
If you want to keep the API as small as possible then here’s some provocative food for thought: If you had ZonedDateTime instead of Instant maybe people wouldn’t be missing Instant because ZonedDateTime in UTC handles that use-case already.
The question is where you want to make distinctions explicit via types and maybe where you want to provide common base types to allow abstracting the distinction away.
If you want to keep the API as small as possible then here’s some provocative food for thought: If you had ZonedDateTime instead of Instant maybe people wouldn’t be missing Instant because ZonedDateTime in UTC handles that use-case already.
The question is where you want to make distinctions explicit via types and maybe where you want to provide common base types to allow abstracting the distinction away.
The use-case for instant is different: It should be use to record past events that may not change (e.g. the Unix timestamp cannot change) while ZonedDateTimes can (e.g. if laws change), which is why they should be used to record things like future appointments, so maybe not impossible to use a common type, you have to be extra careful to not break those functionalities.
You mean that laws can change the meaning of the past for a ZonedDateTime (in UTC)?
You mean that laws can change the meaning of the past for a ZonedDateTime (in UTC)?
Yes. For UTC this might not be a problem, but for other timezones it might (see the efforts on DST). Just remember to think about all the use cases before trying to implement something.
That’s the point I was trying to provoke. ZonedDateTime in UTC can handle the Instant use-case.
Personally, I’d only still want to have the distinction of both types so I can enforce the UTC timezone via types where that is critical.