Skip to content

Add an OffsetDateTime type #90

Closed
Closed
@cromefire

Description

@cromefire

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)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    formattersRelated to parsing and formatting

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions