Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Added ZonedDateTime #175

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions core/common/src/ZonedDateTime.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package kotlinx.datetime

/** A timezone-aware date-time object. */
public sealed class ZonedDateTime(
protected val localDateTime: LocalDateTime,
) : Comparable<ZonedDateTime> {

public abstract val timeZone: TimeZone

// XXX: If the underlying time zone database can change while the current process is running
// this value could become incorrect. Maybe don't cache at all? Or detect time zone db changes?
private val instant: Instant by lazy { localDateTime.toInstant(timeZone) }

public val year: Int get() = localDateTime.year
public val monthNumber: Int get() = localDateTime.monthNumber
public val month: Month get() = localDateTime.month
public val dayOfMonth: Int get() = localDateTime.dayOfMonth
public val dayOfWeek: DayOfWeek get() = localDateTime.dayOfWeek
public val dayOfYear: Int get() = localDateTime.dayOfYear
public val hour: Int get() = localDateTime.hour
public val minute: Int get() = localDateTime.minute
public val second: Int get() = localDateTime.second
public val nanosecond: Int get() = localDateTime.nanosecond

public fun toInstant(): Instant = instant

public fun toLocalDateTime(): LocalDateTime = localDateTime

public fun toLocalDateTime(timeZone: TimeZone): LocalDateTime =
toInstant().toLocalDateTime(timeZone)

public fun toLocalDate(): LocalDate = toLocalDateTime().date

public fun toLocalDate(timeZone: TimeZone): LocalDate = toLocalDateTime(timeZone).date

override fun compareTo(other: ZonedDateTime): Int = toInstant().compareTo(other.toInstant())

override fun equals(other: Any?): Boolean =
this === other || (other is ZonedDateTime && compareTo(other) == 0)

override fun hashCode(): Int = localDateTime.hashCode() xor timeZone.hashCode()

public companion object {

public fun parse(isoString: String): ZonedDateTime {
TODO()
}
}
}

/** Constructs a new [ZonedDateTime] from the given [localDateTime] and [timeZone]. */
public fun ZonedDateTime(localDateTime: LocalDateTime, timeZone: TimeZone): ZonedDateTime =
when (timeZone) {
is FixedOffsetTimeZone -> OffsetDateTime(localDateTime, timeZone)
// TODO: Define a common RegionTimeZone and make TimeZone a sealed class/interface
else -> RegionDateTime(localDateTime, timeZone)
}

public fun String.toZonedDateTime(): ZonedDateTime = ZonedDateTime.parse(this)

/**
* A [ZonedDateTime] describing a region-based [TimeZone].
*
* This class tries to represent how humans think in terms of dates.
* For example, adding one day will result in the same local time even if a DST change happens
* within that day.
* Also, you can safely represent future dates because time zone database changes are taken into
* account.
*/
public class RegionDateTime(
localDateTime: LocalDateTime,
// TODO: this should be a RegionTimeZone
override val timeZone: TimeZone,
// TODO: Add optional DST offset or at least a UTC offset (should it be part of RegionTimeZone?)
wkornewald marked this conversation as resolved.
Show resolved Hide resolved
) : ZonedDateTime(localDateTime) {

public constructor(instant: Instant, timeZone: TimeZone) :
this(instant.toLocalDateTime(timeZone), timeZone)

// TODO: Should RegionTimeZone.toString() print with surrounding `[]`?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any standards for this?

Copy link
Contributor Author

@wkornewald wkornewald Jan 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgreSQL, Elasticsearch, java.time, JS Temporal and others seem to have settled on this full format: 2021-03-28T03:16:20+02:00[Europe/Berlin]

Though, maybe RegionTimeZone itself should just be Europe/Berlin and the surrounding [] would be added by RegionDateTime. Most libs will parse the region without the [].

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case I'd settle with the established format

override fun toString(): String = "$localDateTime[$timeZone]"
}

/**
* A [ZonedDateTime] with a [FixedOffsetTimeZone]. Use this only for representing past events.
*
* Don't use this to represent future dates (e.g. in a calendar) because this fails to work
* correctly under time zone database changes. Use [RegionDateTime] instead.
*/
public class OffsetDateTime(
localDateTime: LocalDateTime,
override val timeZone: FixedOffsetTimeZone,
) : ZonedDateTime(localDateTime) {

public constructor(instant: Instant, timeZone: FixedOffsetTimeZone) :
this(instant.toLocalDateTime(timeZone), timeZone)

override fun toString(): String = "$localDateTime$timeZone"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be in RFC3339 as that's the common serialization and representation everywhere.

}
30 changes: 30 additions & 0 deletions core/common/test/ZonedDateTimeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kotlinx.datetime

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs

internal class ZonedDateTimeTest {
@Test
fun parseZonedDateTime() {
val offsetDates = listOf(
"2021-12-29 17:32:01Z",
"2021-12-29T17:32:01Z",
"2021-12-29 17:32:01+03:00",
"2021-12-29 17:32:01-03:00",
)
val regionDates = listOf(
"2021-12-29 17:32:01[Europe/Berlin]",
"2021-12-29 17:32:01+01:00[Europe/Berlin]",
)
for (isoString in offsetDates + regionDates) {
val dateTime = ZonedDateTime.parse(isoString)
assertEquals(isoString.replace(" ", "T"), dateTime.toString())
if (isoString in offsetDates) {
assertIs<OffsetDateTime>(dateTime)
} else {
assertIs<RegionDateTime>(dateTime)
}
}
}
}