|
/* |
|
MIT License |
|
|
|
Copyright (c) 2023 Daniel Rendox |
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
of this software and associated documentation files (the "Software"), to deal |
|
in the Software without restriction, including without limitation the rights |
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
copies of the Software, and to permit persons to whom the Software is |
|
furnished to do so, subject to the following conditions: |
|
|
|
The above copyright notice and this permission notice shall be included in all |
|
copies or substantial portions of the Software. |
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
SOFTWARE. |
|
|
|
This file includes code that is licensed under Apache License Version 2.0 |
|
*/ |
|
|
|
import kotlinx.datetime.Clock |
|
import kotlinx.datetime.FixedOffsetTimeZone |
|
import kotlinx.datetime.Instant |
|
import kotlinx.datetime.LocalDateTime |
|
import kotlinx.datetime.TimeZone |
|
import kotlinx.datetime.UtcOffset |
|
import kotlinx.datetime.toInstant |
|
import kotlinx.datetime.toLocalDateTime |
|
|
|
/* |
|
Disclaimer: This is not an official documentation for kotlinx-datetime library. And it may contain mistakes. |
|
*/ |
|
|
|
/** |
|
* Local time is not local at all. A more fitting term would be "clock time." Meaning that |
|
* it will not change when the DST gets introduced or revoked. |
|
* It will be local only if you make it local, although it never stores [TimeZone] info. |
|
*/ |
|
fun m1() { |
|
val utcTimeInstant = Instant.parse("2023-08-07T01:00:00Z") // z means zero offset (UTC+0 or simply UTC) |
|
val utcClockTime = utcTimeInstant.toLocalDateTime(TimeZone.UTC) |
|
println(utcClockTime) // 2023-08-07T01:00 |
|
} |
|
|
|
/** |
|
* I used to think that [TimeZone.currentSystemDefault] somehow updates itself or |
|
* a special method gets called on a device when a DST gets introduced/revoked. |
|
* It doesn't. [TimeZone] only defines the rules for time conversion. |
|
* |
|
* So if you create two [Instant]s, one observing DST and the other not observing |
|
* DST and convert them two the local time, you'll notice that extra hour difference. |
|
* Whereas the current time zone rules stay the same. |
|
* |
|
* However, someone will update these rules if any country changes them. |
|
*/ |
|
fun m2() { |
|
val currentTimeZone = TimeZone.currentSystemDefault() |
|
val summerTime = Instant.parse("2023-08-07T01:00:00Z") |
|
val winterTime = Instant.parse("2023-12-07T01:00:00Z") |
|
val localSummerTime = summerTime.toLocalDateTime(currentTimeZone) |
|
val localWinterTime = winterTime.toLocalDateTime(currentTimeZone) |
|
println(localSummerTime) // my result: 2023-08-07T04:00 |
|
println(localWinterTime) // my result: 2023-12-07T03:00 |
|
} |
|
|
|
/** |
|
* Contrary to [m2], [m3]'s results differ only by the month number. |
|
* |
|
* That's because |
|
* the time is stored in [LocalDateTime] not in [Instant]. [Instant] stores the time |
|
* using the number of nanoseconds since the epoch. Whereas [LocalDateTime] only stores |
|
* the numbers you provide in the constructor and they never change regardless of [TimeZone]. |
|
*/ |
|
fun m3() { |
|
val summerTime = LocalDateTime(2023, 8, 7, 1, 0, 0, 0) |
|
val winterTime = LocalDateTime(2023, 12, 7, 1, 0, 0, 0) |
|
println(summerTime) // 2023-08-07T01:00 |
|
println(winterTime) // 2023-12-07T01:00 |
|
} |
|
|
|
/** |
|
* This method shows how to store the time of past events. Or events that will definitely happen in a |
|
* well-defined instant of time in the future not far away from now (like an order confirmation |
|
* deadline in 1 hour from now). |
|
* |
|
* Store the time of such events in UTC [Instant]. It will be easily converted when a user |
|
* moves to another time zone. |
|
* |
|
* However, you can store it [LocalDateTime] along with the [TimeZone] as is done for |
|
* future events and shown in [m6], but that introduces one more entry and additional hassle. |
|
*/ |
|
fun m4() { |
|
// Run this code when the event happens |
|
val storedTime = Clock.System.now() |
|
// When you need to display the value to user |
|
val displayValueInKyiv = storedTime.toLocalDateTime(TimeZone.of("Europe/Kiev")) |
|
val displayValueInBerlin = storedTime.toLocalDateTime(TimeZone.of("Europe/Berlin")) |
|
println(displayValueInKyiv) // my result: 2023-08-07T19:16:53.197945500 |
|
println(displayValueInBerlin) // my result: 2023-08-07T18:16:53.197945500 |
|
} |
|
|
|
/** |
|
* This method explains why storing time in UTC for future events will cause problems when a country |
|
* changes their time zone or decides to introduce/revoke DST. |
|
* |
|
* The correct approach to storing the time of scheduled events is in the [m6]. |
|
* |
|
* This is a working example from [Introducing kotlinx-datetime by Ilya Gorbunov](https://youtu.be/YwN0kAMNvXI?t=733). |
|
* The original code (definitely check it to understand the example): |
|
* ``` |
|
* val localTz = TimeZone.currentSystemDefault() // Europe/Berlin: UTC+2 |
|
* val meetingStarts = LocalDateTime(2025, Month.AUGUST, 23, 13, 00) |
|
* val startInstant = meetingStarts.toInstant(localTz) // 2025-08-13T11:00:00Z |
|
* ... 5 years later ... |
|
* val startInstant = Instant.parse("2025-08-13T11:00:00Z") |
|
* val localTz = TimeZone.currentSystemDefault() // Europe/Berlin: UTC+1 |
|
* val meetingStarts = startInstant.toLocalDateTime(localTz) // 2025-08-13T12:00 |
|
* ``` |
|
*/ |
|
fun m5() { |
|
// Let's assume that the DST is absent but the country changes its time zone in the future. |
|
val initialTimeZone = FixedOffsetTimeZone(UtcOffset(hours = 2)) |
|
val changedTimeZone = FixedOffsetTimeZone(UtcOffset(hours = 1)) |
|
// The event should start at 13:00, so we store the following UTC value |
|
val storedValue = Instant.parse("2025-08-13T11:00:00Z") |
|
val expectedValue = storedValue.toLocalDateTime(initialTimeZone) |
|
val realValue = storedValue.toLocalDateTime(changedTimeZone) |
|
println(expectedValue) // 2025-08-13T13:00 |
|
println(realValue) // 2025-08-13T12:00 |
|
/* |
|
The real and the expected values are different. The problem is not with the outdated |
|
timezone. The time zone does get updated. But the problem is that the storedValue is a UTC |
|
Instant that only stores the number of nanos since the epoch. This number doesn't change when |
|
a TimeZone changes. |
|
|
|
The confusion here is that if we used LocalDateTime instead, it wouldn't change either, |
|
but the program would work properly. That's because the LocalDateTime stores the "clock time", |
|
that's what should be the source of truth. |
|
*/ |
|
} |
|
|
|
/** |
|
* Currently the latest version of kotlinx-datetime is 0.4.0 and |
|
* [it doesn't have ZonedDateTime out of the box](https://github.com/Kotlin/kotlinx-datetime/issues/163). |
|
* So it's more convenient to define our custom solution for now. |
|
* |
|
* This is an example of how to properly store events scheduled for future. |
|
* [timeZone] should normally be private and immutable, cause it's value gets updated automatically |
|
* when some country changes their TimeZone. But the example in the [m6] is imaginary and the Berlin's |
|
* TimeZone doesn't change in reality, we change it manually instead. |
|
*/ |
|
class MyZonedDateTime( |
|
private val localTime: LocalDateTime, |
|
var timeZone: TimeZone, |
|
) { |
|
/** |
|
* Technically, you could just return [localTime] in this method. But we make this conversion |
|
* cause when the user moves to a different TimeZone, they expect a different time to be displayed. |
|
*/ |
|
fun displayTime(newTimeZone: TimeZone): String { |
|
val initialTime = localTime.toInstant(timeZone) |
|
val resultingLocalTime = initialTime.toLocalDateTime(newTimeZone) |
|
return resultingLocalTime.toString() |
|
} |
|
} |
|
|
|
/** |
|
* Here is the corrected solution from [m5] |
|
* |
|
* So when Berlin changes their TimeZone, the event time shouldn't change. It will still happen |
|
* at the arranged time. |
|
* However, the physical time of the event will differ from the arranged one. That's why other |
|
* countries will observe the change. |
|
*/ |
|
fun m6() { |
|
// define constants (just for the example) |
|
val kyivTimeZone = TimeZone.of("Europe/Kiev") |
|
val berlinTimeZoneCurrent = TimeZone.of("UTC+2") |
|
val berlinTimeZoneChanged = TimeZone.of("UTC+1") |
|
|
|
val eventTime = LocalDateTime(2025, 8, 13, 13,0,0) |
|
val zonedDateTime = MyZonedDateTime(eventTime, berlinTimeZoneCurrent) |
|
|
|
println("Event time showed in Berlin: ${zonedDateTime.displayTime(berlinTimeZoneCurrent)}") |
|
println("Event time showed in Kyiv: ${zonedDateTime.displayTime(kyivTimeZone)}") |
|
|
|
println() |
|
println("Berlin's TimeZone changes") |
|
println() |
|
zonedDateTime.timeZone = berlinTimeZoneChanged |
|
|
|
println("Event time showed in Berlin: ${zonedDateTime.displayTime(berlinTimeZoneChanged)}") |
|
println("Event time showed in Kyiv: ${zonedDateTime.displayTime(kyivTimeZone)}") |
|
} |