Closed
Description
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. OpenAPITools/openapi-generator#7353 (comment)).
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)
}
}