Skip to content

Normalize to FixedOffsetTimeZone in JVM and JS #130

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

Merged
merged 2 commits into from
Jul 8, 2021
Merged
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
9 changes: 7 additions & 2 deletions core/common/test/TimeZoneTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,16 @@ class TimeZoneTest {
@Test
fun utcOffsetNormalization() {
val sameOffsetTZs = listOf("+04", "+04:00", "UTC+4", "UT+04", "GMT+04:00:00").map { TimeZone.of(it) }
val instant = Instant.fromEpochSeconds(0)
val offsets = sameOffsetTZs.map { it.offsetAt(instant) }
for (tz in sameOffsetTZs) {
assertIs<FixedOffsetTimeZone>(tz)
}
val offsets = sameOffsetTZs.map { (it as FixedOffsetTimeZone).offset }
val zoneIds = sameOffsetTZs.map { it.id }

assertTrue(offsets.distinct().size == 1, "Expected all offsets to be equal: $offsets")
assertTrue(offsets.map { it.toString() }.distinct().size == 1, "Expected all offsets to have the same string representation: $offsets")

assertTrue(zoneIds.distinct().size > 1, "Expected some fixed offset zones to have different ids: $zoneIds")
}

// from 310bp
Expand Down
29 changes: 10 additions & 19 deletions core/darwin/src/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ private fun systemDateByLocalDate(zone: NSTimeZone, localDate: NSDate): NSDate?
return iso8601.dateFromComponents(dateComponents)
}

internal actual class PlatformTimeZoneImpl(private val value: NSTimeZone, override val id: String): TimeZoneImpl {
internal actual class RegionTimeZone(private val value: NSTimeZone, actual override val id: String): TimeZone() {
actual companion object {
actual fun of(zoneId: String): PlatformTimeZoneImpl {
actual fun of(zoneId: String): RegionTimeZone {
val abbreviations = NSTimeZone.abbreviationDictionary
val trueZoneId = abbreviations[zoneId] as String? ?: zoneId
val zone = NSTimeZone.timeZoneWithName(trueZoneId)
?: throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'")
return PlatformTimeZoneImpl(zone, zoneId)
return RegionTimeZone(zone, zoneId)
}

actual fun currentSystemDefault(): PlatformTimeZoneImpl {
actual fun currentSystemDefault(): RegionTimeZone {
/* The framework has its own cache of the system timezone. Calls to
[NSTimeZone systemTimeZone] do not reflect changes to the system timezone
and instead just return the cached value. Thus, to acquire the current
Expand Down Expand Up @@ -86,7 +86,7 @@ internal actual class PlatformTimeZoneImpl(private val value: NSTimeZone, overri
*/
NSTimeZone.resetSystemTimeZone()
val zone = NSTimeZone.systemTimeZone
return PlatformTimeZoneImpl(zone, zone.name)
return RegionTimeZone(zone, zone.name)
}

actual val availableZoneIds: Set<String>
Expand All @@ -110,7 +110,7 @@ internal actual class PlatformTimeZoneImpl(private val value: NSTimeZone, overri
}
}

override fun atStartOfDay(date: LocalDate): Instant {
actual override fun atStartOfDay(date: LocalDate): Instant {
val ldt = LocalDateTime(date, LocalTime.MIN)
val epochSeconds = ldt.toEpochSecond(UtcOffset.ZERO)
// timezone
Expand All @@ -132,15 +132,15 @@ internal actual class PlatformTimeZoneImpl(private val value: NSTimeZone, overri
return Instant(midnight.timeIntervalSince1970.toLong(), 0)
}

override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime {
actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime {
val epochSeconds = dateTime.toEpochSecond(UtcOffset.ZERO)
var offset = preferred?.totalSeconds ?: Int.MAX_VALUE
val transitionDuration = run {
/* a date in an unspecified timezone, defined by the number of seconds since
the start of the epoch in *that* unspecified timezone */
val date = dateWithTimeIntervalSince1970Saturating(epochSeconds)
val newDate = systemDateByLocalDate(value, date)
?: throw RuntimeException("Unable to acquire the offset at $dateTime for zone ${this@PlatformTimeZoneImpl}")
?: throw RuntimeException("Unable to acquire the offset at $dateTime for zone ${this@RegionTimeZone}")
// we now know the offset of that timezone at this time.
offset = value.secondsFromGMTForDate(newDate).toInt()
/* `dateFromComponents` automatically corrects the date to avoid gaps. We
Expand All @@ -155,23 +155,14 @@ internal actual class PlatformTimeZoneImpl(private val value: NSTimeZone, overri
} catch (e: ArithmeticException) {
throw RuntimeException("Anomalously long timezone transition gap reported", e)
}
return ZonedDateTime(correctedDateTime, TimeZone(this), UtcOffset.ofSeconds(offset))
return ZonedDateTime(correctedDateTime, this@RegionTimeZone, UtcOffset.ofSeconds(offset))
}

override fun offsetAt(instant: Instant): UtcOffset {
actual override fun offsetAtImpl(instant: Instant): UtcOffset {
val date = dateWithTimeIntervalSince1970Saturating(instant.epochSeconds)
return UtcOffset.ofSeconds(value.secondsFromGMTForDate(date).toInt())
}

// org.threeten.bp.ZoneId#equals
override fun equals(other: Any?): Boolean =
this === other || other is PlatformTimeZoneImpl && this.id == other.id

// org.threeten.bp.ZoneId#hashCode
override fun hashCode(): Int = id.hashCode()

// org.threeten.bp.ZoneId#toString
override fun toString(): String = id
}

internal actual fun currentTime(): Instant = NSDate.date().toKotlinInstant()
2 changes: 1 addition & 1 deletion core/darwin/test/ConvertersTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ConvertersTest {
for (id in TimeZone.availableZoneIds) {
val normalizedId = (NSTimeZone.abbreviationDictionary[id] ?: id) as String
val timeZone = TimeZone.of(normalizedId)
if (timeZone.value is ZoneOffsetImpl) {
if (timeZone is FixedOffsetTimeZone) {
continue
}
val nsTimeZone = timeZone.toNSTimeZone()
Expand Down
27 changes: 17 additions & 10 deletions core/js/src/TimeZone.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,38 @@ public actual open class TimeZone internal constructor(internal val zoneId: Zone
override fun toString(): String = zoneId.toString()

public actual companion object {
public actual fun currentSystemDefault(): TimeZone = ZoneId.systemDefault().let(::TimeZone)
public actual fun currentSystemDefault(): TimeZone = ofZone(ZoneId.systemDefault())
public actual val UTC: FixedOffsetTimeZone = UtcOffset(jtZoneOffset.UTC).asTimeZone()

public actual fun of(zoneId: String): TimeZone = try {
val zone = ZoneId.of(zoneId)
if (zone is jtZoneOffset) {
FixedOffsetTimeZone(UtcOffset(zone))
} else {
TimeZone(zone)
}
ofZone(ZoneId.of(zoneId))
} catch (e: Throwable) {
if (e.isJodaDateTimeException()) throw IllegalTimeZoneException(e)
throw e
}

private fun ofZone(zoneId: ZoneId): TimeZone = when {
zoneId is jtZoneOffset ->
FixedOffsetTimeZone(UtcOffset(zoneId))
zoneId.rules().isFixedOffset() ->
FixedOffsetTimeZone(UtcOffset(zoneId.normalized() as jtZoneOffset), zoneId)
else ->
TimeZone(zoneId)
}


public actual val availableZoneIds: Set<String> get() = ZoneId.getAvailableZoneIds().toSet()
}
}

@Serializable(with = FixedOffsetTimeZoneSerializer::class)
public actual class FixedOffsetTimeZone actual constructor(public actual val offset: UtcOffset): TimeZone(offset.zoneOffset) {
private val zoneOffset get() = zoneId as jtZoneOffset
public actual class FixedOffsetTimeZone
internal constructor(public actual val offset: UtcOffset, zoneId: ZoneId): TimeZone(zoneId) {

public actual constructor(offset: UtcOffset) : this(offset, offset.zoneOffset)

@Deprecated("Use offset.totalSeconds", ReplaceWith("offset.totalSeconds"))
public actual val totalSeconds: Int get() = zoneOffset.totalSeconds().toInt()
public actual val totalSeconds: Int get() = offset.totalSeconds
}


Expand Down
2 changes: 1 addition & 1 deletion core/jvm/src/Converters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public fun TimeZone.toJavaZoneId(): java.time.ZoneId = this.zoneId
/**
* Converts this [java.time.ZoneId][java.time.ZoneId] value to a [kotlinx.datetime.TimeZone][TimeZone] value.
*/
public fun java.time.ZoneId.toKotlinTimeZone(): TimeZone = TimeZone(this)
public fun java.time.ZoneId.toKotlinTimeZone(): TimeZone = TimeZone.ofZone(this)


/**
Expand Down
23 changes: 15 additions & 8 deletions core/jvm/src/TimeZoneJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,35 @@ public actual open class TimeZone internal constructor(internal val zoneId: Zone
override fun toString(): String = zoneId.toString()

public actual companion object {
public actual fun currentSystemDefault(): TimeZone = ZoneId.systemDefault().let(::TimeZone)
public actual fun currentSystemDefault(): TimeZone = ofZone(ZoneId.systemDefault())
public actual val UTC: FixedOffsetTimeZone = UtcOffset(jtZoneOffset.UTC).asTimeZone()

public actual fun of(zoneId: String): TimeZone = try {
val zone = ZoneId.of(zoneId)
if (zone is jtZoneOffset) {
FixedOffsetTimeZone(UtcOffset(zone))
} else {
TimeZone(zone)
}
ofZone(ZoneId.of(zoneId))
} catch (e: Exception) {
if (e is DateTimeException) throw IllegalTimeZoneException(e)
throw e
}

internal fun ofZone(zoneId: ZoneId): TimeZone = when {
zoneId is jtZoneOffset ->
FixedOffsetTimeZone(UtcOffset(zoneId))
zoneId.rules.isFixedOffset ->
FixedOffsetTimeZone(UtcOffset(zoneId.normalized() as jtZoneOffset), zoneId)
else ->
TimeZone(zoneId)
}

public actual val availableZoneIds: Set<String> get() = ZoneId.getAvailableZoneIds()
}
}

@Serializable(with = FixedOffsetTimeZoneSerializer::class)
public actual class FixedOffsetTimeZone
public actual constructor(public actual val offset: UtcOffset): TimeZone(offset.zoneOffset) {
internal constructor(public actual val offset: UtcOffset, zoneId: ZoneId): TimeZone(zoneId) {

public actual constructor(offset: UtcOffset) : this(offset, offset.zoneOffset)

@Deprecated("Use offset.totalSeconds", ReplaceWith("offset.totalSeconds"))
public actual val totalSeconds: Int get() = offset.totalSeconds
}
Expand Down
14 changes: 14 additions & 0 deletions core/jvm/test/ConvertersTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ class ConvertersTest {
test("Europe/Berlin")
}

@Test
fun fixedOffsetTimeZone() {
val zone = TimeZone.of("UTC") as FixedOffsetTimeZone

val jtZone = zone.toJavaZoneId()
val jtZoneOffset = zone.toJavaZoneOffset()

assertEquals(zone.id, jtZone.id)
assertNotEquals(jtZone, jtZoneOffset)
assertEquals(jtZone.normalized(), jtZoneOffset)

assertIs<FixedOffsetTimeZone>(jtZone.toKotlinTimeZone())
}

@Test
fun zoneOffset() {
fun test(offsetString: String) {
Expand Down
29 changes: 10 additions & 19 deletions core/native/cinterop_actuals/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ import kotlinx.datetime.internal.*
import kotlinx.cinterop.*
import platform.posix.free

internal actual class PlatformTimeZoneImpl(private val tzid: TZID, override val id: String): TimeZoneImpl {
internal actual class RegionTimeZone(private val tzid: TZID, actual override val id: String): TimeZone() {
actual companion object {
actual fun of(zoneId: String): PlatformTimeZoneImpl {
actual fun of(zoneId: String): RegionTimeZone {
val tzid = timezone_by_name(zoneId)
if (tzid == TZID_INVALID) {
throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'")
}
return PlatformTimeZoneImpl(tzid, zoneId)
return RegionTimeZone(tzid, zoneId)
}

actual fun currentSystemDefault(): PlatformTimeZoneImpl = memScoped {
actual fun currentSystemDefault(): RegionTimeZone = memScoped {
val tzid = alloc<TZIDVar>()
val string = get_system_timezone(tzid.ptr)
?: throw RuntimeException("Failed to get the system timezone.")
val kotlinString = string.toKString()
free(string)
PlatformTimeZoneImpl(tzid.value, kotlinString)
RegionTimeZone(tzid.value, kotlinString)
}

actual val availableZoneIds: Set<String>
Expand All @@ -45,7 +45,7 @@ internal actual class PlatformTimeZoneImpl(private val tzid: TZID, override val
}
}

override fun atStartOfDay(date: LocalDate): Instant = memScoped {
actual override fun atStartOfDay(date: LocalDate): Instant = memScoped {
val ldt = LocalDateTime(date, LocalTime.MIN)
val epochSeconds = ldt.toEpochSecond(UtcOffset.ZERO)
val midnightInstantSeconds = at_start_of_day(tzid, epochSeconds)
Expand All @@ -55,13 +55,13 @@ internal actual class PlatformTimeZoneImpl(private val tzid: TZID, override val
Instant(midnightInstantSeconds, 0)
}

override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = memScoped {
actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = memScoped {
val epochSeconds = dateTime.toEpochSecond(UtcOffset.ZERO)
val offset = alloc<IntVar>()
offset.value = preferred?.totalSeconds ?: Int.MAX_VALUE
val transitionDuration = offset_at_datetime(tzid, epochSeconds, offset.ptr)
if (offset.value == Int.MAX_VALUE) {
throw RuntimeException("Unable to acquire the offset at $dateTime for zone ${this@PlatformTimeZoneImpl}")
throw RuntimeException("Unable to acquire the offset at $dateTime for zone ${this@RegionTimeZone}")
}
val correctedDateTime = try {
dateTime.plusSeconds(transitionDuration)
Expand All @@ -70,26 +70,17 @@ internal actual class PlatformTimeZoneImpl(private val tzid: TZID, override val
} catch (e: ArithmeticException) {
throw RuntimeException("Anomalously long timezone transition gap reported", e)
}
ZonedDateTime(correctedDateTime, TimeZone(this@PlatformTimeZoneImpl), UtcOffset.ofSeconds(offset.value))
ZonedDateTime(correctedDateTime, this@RegionTimeZone, UtcOffset.ofSeconds(offset.value))
}

override fun offsetAt(instant: Instant): UtcOffset {
actual override fun offsetAtImpl(instant: Instant): UtcOffset {
val offset = offset_at_instant(tzid, instant.epochSeconds)
if (offset == Int.MAX_VALUE) {
throw RuntimeException("Unable to acquire the offset at instant $instant for zone $this")
}
return UtcOffset.ofSeconds(offset)
}

// org.threeten.bp.ZoneId#equals
override fun equals(other: Any?): Boolean =
this === other || other is PlatformTimeZoneImpl && this.id == other.id

// org.threeten.bp.ZoneId#hashCode
override fun hashCode(): Int = id.hashCode()

// org.threeten.bp.ZoneId#toString
override fun toString(): String = id
}

internal actual fun currentTime(): Instant = memScoped {
Expand Down
Loading