kevin.cianfarini
04/29/2025, 3:19 PMkotlinx.datetime.TimeZone
shouldn’t query the underlying database from the tzdb entry when calling TimeZone.of(String)
. Do you have thoughts?kevin.cianfarini
04/29/2025, 3:19 PMkevin.cianfarini
04/29/2025, 3:23 PMpublic interface ZonedClock : Clock {
public fun now(): Instant
public fun currentTimeZone(): TimeZone
public fun queryTimeZone(id: TimeZoneId): TimeZone
}
kevin.cianfarini
04/29/2025, 3:27 PMCLOVIS
04/29/2025, 3:27 PMTimeZone
, and the constructor doesn't throw an exception, I expect that it is a valid timezone that exists and I can use. It would be very surprising if it threw later on during some other call. If I wanted to store a timezone without validation, I would use a String
.CLOVIS
04/29/2025, 3:28 PMCLOVIS
04/29/2025, 3:28 PMTimeZone
class that doesn't validate its content is. How is it different from a String
?kevin.cianfarini
04/29/2025, 3:31 PMTimeZone
would only really be possible to acquire from a service, like a ZonedClock
.
• Your contention seems to be with TimeZoneId
which would wrap a IANA tzdb id. I think that’s a fair criticism that there’s not much use for that type if it doesn’t validate its content’s form at all. I think it would only make sense to include a type like that if some sort of validation could be supplied (but doesn’t involve querying the filesystem’s tzdb).kevin.cianfarini
04/29/2025, 3:33 PMjw
04/29/2025, 3:33 PMjw
04/29/2025, 3:34 PMkevin.cianfarini
04/29/2025, 3:35 PMjw
04/29/2025, 3:35 PMCLOVIS
04/29/2025, 3:35 PMkevin.cianfarini
04/29/2025, 3:37 PMCLOVIS
04/29/2025, 3:37 PMIn many places Kotlin’s made a clean break from this,Do you have examples? Because
<http://kotlin.io|kotlin.io>
adds a lot of extensions that make java.nio
more implicit by removing the explicit calls to Paths
, Files
, etc. For example, Kotlin adds Path.deleteRecursively
, which Java doesn't have (IIRC).kevin.cianfarini
04/29/2025, 3:37 PMSystemFileSystem
being a requirement, and not something you can avoid with static function calls.kevin.cianfarini
04/29/2025, 3:38 PMInstant.now()
is another example in favor of Clock
is another example.CLOVIS
04/29/2025, 3:39 PMTimeZone
could contain an invalid timezone, no? Still, looking up a system configuration file is nothing like a network request.jw
04/29/2025, 3:39 PMPath
, the file system is carried along.jw
04/29/2025, 3:39 PMTimeZone
thoughkevin.cianfarini
04/29/2025, 3:39 PMCLOVIS
04/29/2025, 3:41 PMnew
to load the JAR. What is your opinion on this and how it relates to this topic?
(I think it can even do network requests)jw
04/29/2025, 3:42 PMkevin.cianfarini
04/29/2025, 3:42 PMCLOVIS
04/29/2025, 3:42 PMkevin.cianfarini
04/29/2025, 3:43 PMClock
is a really great example here, likewise so is a potential for ZonedClock
which is how you might acquire the system’s current timezone versus doing TimeZone.currentSystemDefault
.kevin.cianfarini
04/29/2025, 3:43 PMCLOVIS
04/29/2025, 3:48 PMOnce you have aBut that's what the article is against, no? The article would want a file-system-agnostic, the file system is carried along.Path
Path
that is a pure identifier and has no behavior, and then a FileSystem
interface which contains the different methods for working with files, each taking a Path
as a parameter.
Applied to this example, this means TimeZone
would be a pure identifier (with no logic or validation) and you would have a TimeZoneDatabase
which contains the methods. It would be possible of having invalid TimeZone
instances (explicitly mentioned by the author as the reason they want this to be changed in KotlinX-Datetime). What's the point of having a TimeZone
class then?jw
04/29/2025, 3:50 PMjw
04/29/2025, 3:56 PMjw
04/29/2025, 3:58 PMkevin.cianfarini
04/29/2025, 4:14 PMBut that’s what the article is against, no? The article would want a file-system-agnosticThis is the design of both Okio and kotlinx-io, just for reference.that is a pure identifier and has no behavior, and then aPath
interface which contains the different methods for working with files, each taking aFileSystem
as a parameter.Path
kevin.cianfarini
04/29/2025, 4:16 PMApplied to this example, this meansYou’re conflating what I recommended above —would be a pure identifier (with no logic or validation) and you would have aTimeZone
which contains the methods. It would be possible of having invalidTimeZoneDatabase
instances (explicitly mentioned by the author as the reason they want this to be changed in KotlinX-Datetime). What’s the point of having aTimeZone
class then?TimeZone
TimeZone
(the thing which has offset rules) would be separate from any sort of IANA identifier.Dmitry Khalanskiy [JB]
04/29/2025, 4:23 PMTimeZoneId
is just exactly a String
with no additional logic: we don't know how to correctly validate a TimeZoneId
, we can't extract any meaningful information from it. This looks like a completely useless abstraction, therefore we don't introduce it.kevin.cianfarini
04/29/2025, 4:27 PMpublic interface ZonedClock : Clock {
public fun now(): Instant
public fun currentTimeZone(): TimeZone
public fun queryTimeZone(id: String): TimeZone
}
and also get rid of TimeZone parsing and TimeZone.of
.
I think the main bit the post points out is that parsing a network response with a tz identifier shouldn’t throw an unknwon tz exception because it’s not in the JVM’s tzdata. Use cases for this include never operating on an Instant
with that parsed timezone, but instead forwarding it to another service. The value part of the tz identifier is the important part for this use case, not the service part which loads offset rules from some data source.kevin.cianfarini
04/29/2025, 4:28 PMI need to build a report that summarizes the emails that our service sends each
day. The input is a set ofrecords:SentEmail
```data class SentEmail(
val customerId: Id,
val timeZone: TimeZone,
val locale: Locale,
val emailAddressId: Id,
val templateId: TemplateId,
val enqueuedAt: Instant,
val deliveredAt: Instant,
)```
Theclass looks like an innocuous way to track a string likeTimeZone
“America/New_York” in a type-safe way.
Unfortunately, Kotlin’s time zone class throws when it’s given a time zone that
isn’t in its host JVM's time zone database. My report will crash if
any customer uses(renamed fromEurope/Kyiv
in 2022).Europe/Kiev
```java.time.zone.ZoneRulesException: Unknown time-zone ID: Europe/Kyiv
at java.time.zone.ZoneRulesProvider.getProvider(ZoneRulesProvider.java)
at java.time.zone.ZoneRulesProvider.getRules(ZoneRulesProvider.java)
at java.time.ZoneRegion.ofId(ZoneRegion.java)
at java.time.ZoneId.of(ZoneId.java)
at kotlinx.datetime.TimeZone$Companion.of(TimeZoneJvm.kt)```
I can fix this crash by updating my JVM to one with more up-to-date time zone
data. Even when I only need an identifier,always loads the offsetTimeZone
rules.this is the entire section on TimeZone fwiw. It’s short.
Dmitry Khalanskiy [JB]
04/29/2025, 4:29 PMat kotlinx.datetime.TimeZone$Companion.of(TimeZoneJvm.kt)That's the problem. If you do not want to construct a
TimeZone
object, do not call TimeZone.of
, keep it a String
in your code.kevin.cianfarini
04/29/2025, 4:33 PMTimeZoneSerializer
https://kotlinlang.org/api/kotlinx-datetime/kotlinx-datetime/kotlinx.datetime.serializers/-time-zone-serializer/
> A serializer for TimeZone that represents the time zone as its identifier.
> JSON example: "Europe/Berlin"
This makes no mention that this serializer will look up a TimeZone
from your local tzdata from an identifier. Also, I don’t think the fix to this is to update the documentation for the serializer or TimeZone
, but perhaps consider an API which doesn’t encourage implicit static dependencies to the platform’s tzdata. What do you think?Dmitry Khalanskiy [JB]
04/29/2025, 4:37 PMtypealias TimeZoneId = String
. Why introduce code with no behavior?kevin.cianfarini
04/29/2025, 4:41 PMpublic interface ZonedClock : Clock {
public fun now(): Instant
public fun defaultTimeZone(): TimeZone
public fun queryTimeZone(id: String): TimeZone
}
This above example builds on several things:
1. ZonedClock from this Github issue.
2. The OP blog post which expresses frustration that deserialization shouldn’t also query the underlying system’s tzdata.
With this API TimeZone.of
would be deprecated like Instant.now
because both of those functions fulfill service obligations. They call out to the system somehow in ways that might be unexpected under test or would would be nice to have easily configurable. Querying for both the system’s default time zone and acquiring any TimeZone
would be done through this ZonedClock
service object which would presumably also have ZonedClock.SYSTEM
which referenced the underlying system’s clock, default tz, and tz database.kevin.cianfarini
04/29/2025, 4:43 PMkevin.cianfarini
04/29/2025, 4:46 PMobject TimeZoneSerializer
even make sense with this constraint? No, because the implicit static dependency on ZonedClock.SYSTEM
is immediately obvious. It would probably need to be injected into the serializer somehow and influence APIs downstream which also currently suffer from this issue.jw
04/29/2025, 4:47 PMTimeZone
in your serialization models in the first place.kevin.cianfarini
04/29/2025, 4:48 PMDmitry Khalanskiy [JB]
04/29/2025, 4:53 PMLocalDateTime
corresponded to a given Instant
from the point of view of the timezone database from 2008. The correct solution to that is not an interface-based system inconveniencing everyone attempting to (de)serialize a value but a ServiceLoader
-style way of introducing a single timezone database to your code.
shouldn't be usingSerialization doesn't only happen across network boundaries. You can also exchange values with your database, or save them to the disk. There's no viable alternative to timezone identifiers in many cases: realistically, you may not have the GPS coordinates of an event, and the timezone identifier is the next best thing.in your serialization modelsTimeZone
kevin.cianfarini
04/29/2025, 5:01 PMZonedClock.defaultTimeZone
3. This unlocks more natural ways to update tzdata — eg. the core kotlinx-datetime module could just rely on the system’s tzdata (if any is present) and a separate module could provide a ZonedClock
implementation that queries a separate source of tzdata that isn’t the system. This is a much simpler pathway to upgrade tzdata than how the JVM currently expects you to update tzdata. This feels particularly useful for contexts like WASM and JS which don’t always allow access to system tzdata, and also for people encountering the need to upgrade tzdata for the first time. It’s just a new dependency versus dealing with something like tzupdater
. I imagine this will also become valuable for people writing code that operates in a multiplatform context — a single common Kotlin dependency versus many different ways of updating tzdata for many different platforms.jw
04/29/2025, 5:11 PMkevin.cianfarini
04/29/2025, 5:12 PMjw
04/29/2025, 5:19 PMDmitry Khalanskiy [JB]
04/29/2025, 5:23 PMWould it be possible to open up that investigation again but publiclyHere's a puzzle since you're interested to demonstrate how tricky this issue is:
val tz1 = systemClock1.queryTimeZone("Europe/Berlin")
val tz2 = systemClock2.queryTimeZone("Europe/Berlin")
println(tz1.toString())
println(tz1 == tz2)
Consider two cases: systemClock1
either has the same timezone data as systemClock2
or something different.
1. What should toString
print? Just the ID? Then two values will look the same in the debugger/the logs. The whole set of rules? Then toString
will be massive.
2. When should tz1
equal tz2
? If just the ID is compared, then two equal objects exhibit different behavior. Should their timezone databases also be compared? Then an innocent lookup of a TimeZone
in a hash table will involve dozens of comparisons of complete timezone database, the banana-monkey-jungle problem: https://softwareengineering.stackexchange.com/questions/368797/sample-code-to-explain-banana-monkey-jungle-problem-by-joe-armstrong . Should timezone databases be compared, but with ===
? Then, trivial wrappers (by
-delegation, for example) around ZonedClock
will break the behavior.
I feel this approach is pretty much unsalvageable without introducing unreasonable complexity that no one actually needs.
You don’t run into the network serialization issue.How come? Do you propose to serialize the whole set of timezone transition rules along with every
ZonedDateTime
-like entity?
The JVM also currently requires a restart, which can be annoyingCould you elaborate? Does someone run their servers for months at a time and wants to be able update the timezone database without stopping them?
kevin.cianfarini
04/29/2025, 5:28 PMSYSTEM[America/New_York]
, or kotlinx-datetime-iana-2025[America/New_York]
.
2. I’m not intimately familiar with the structure of the rules for a timezone, but presumably you’d want to compare not only the IDs (which is just the “identifier” part of the TimeZone) but also its rules like when DST occurs. These could be different when querying two different tzdb sources. These rules would be resolved I think when querying from the identifier with ZonedClock.queryTimeZone
so the actual equality check wouldn’t do any querying.
a. This would likely require that TimeZone has some internal structure which stores these offsets in memory and are required to construct a tzdb timezone.
>> You don’t run into the network serialization issue.
> How come? Do you propose to serialize the whole set of timezone transition rules along with every ZonedDateTime
-like entity?
Let me clarify — the network serialization issue has a manageable resolution. Currently, updating the system’s tzdata doesn’t scale across the many different platforms that Kotlin offers.jw
04/29/2025, 5:28 PMDmitry Khalanskiy [JB]
04/29/2025, 5:35 PMI think you’d want to include information about where the tz was sourced fromYes. Next, how do we ensure that there aren't two timezone databases with the same name? Also, now, every
ZonedClock
has to have a val name: String
for some reason (which is just a nuisance for everyone who just wants the most up-to-date tzdb).
the actual equality check wouldn’t do any queryingYes, but you would still need to compare a lot of historical data that's already loaded into the timezone object. For example,
America/New_York
is 3.5 Kb of binary data.
Let me clarify — the network serialization issue has a manageable resolution.I'm not sure what you mean. If we had a multiplatform
kotlinx-datetime-tzdata
artifact with the latest timezone database information, would that be sufficient?jw
04/29/2025, 5:40 PMkevin.cianfarini
04/29/2025, 5:43 PMZonedClock
has to have a val name: String
for some reason (which is just a nuisance for everyone who just wants the most up-to-date tzdb).
I don’t agree with either of these points. A TimeZone could store the following information:
class TimeZone(
val source: String,
val id: String,
val rules: SomeRulesType
)
Whoever is implementing a ZonedClock
would be required to supply a source, and it’s their problem if they specify something that isn’t meaningful. Kotlinx-datetime’s implementation of source
I think would correspond to the artifact version that offers IANA tzdb data separately, eg. kotlinx-datetime-iana-2025[America/New_York]
where 2025
represents a version of that artifact.
> Yes, but you would still need to compare a lot of historical data that’s already loaded into the timezone object. For example, America/New_York
is 3.5 Kb of binary data.
I had no idea it was that much data! That’s surprising. This again leads me to believe that even just a TimeZone as it’s currently incepted mixes service and identifier responsibilities in so much as it will dynamically look up rules to avoid loading 3.5kb of data into memory. I imagine Jake might have something interesting to say since he’s typing.
> I’m not sure what you mean. If we had a multiplatform kotlinx-datetime-tzdata
artifact with the latest timezone database information, would that be sufficient?
It would help but it doesn’t solve every problem raised in this thread. Still, it’d be helpful and I know there’s another issue tracking this on Github which I’ve been following.jw
04/29/2025, 5:51 PMFile
when now that is inside a zip or in memory and actually it can't.
I fully expect my production application to interact with the system tzdb most of the time, but sometimes it needs to load the tzdb from my own source, or synthesize interesting dst transitions (or whatever) in tests. And that problem is rarely in my system, but a library that assumes it can call TimeZone.of
when i'm trying to load from a custom db.
Now, do people who build libraries that talk to the file system still do things like write SystemFileSystem.read(externalPath)
? Absolutely. But the argument that they should hoist that dependency into their entrypoint is easy to make because the file system API is designed to encourage that.
Someone who builds a library that talks to a datetime library is inevitably going to do Clock.SYSTEM.now()
, and I believe once again that the argument for them hoisting clock is easy.
But if that same person also calls TimeZone.of
never had the opportunity for inversion of control. The API doesn't guide them to hoisting that implicit system dependency. Every library would need to invent their own abstraction, assuming they could even be convinced in the first place.kevin.cianfarini
04/29/2025, 5:54 PMclass TimeZone(
val id: String,
private val tzdbResolver: TzResolver
)
Where that resolver knows how to query the underlying rules and specifies some metadata like where the id is being sourced from. Equality of two timezones should likely also delegate to equality of the resolver which could specify things like the name of the data source and if the tzdata files are equivalent.kevin.cianfarini
04/29/2025, 5:55 PMjava.nio.Path
which will include an implicit FileSystem in it based on how that path is acquired.kevin.cianfarini
04/29/2025, 5:55 PMPaths.get
which has an implicit dependency on the system’s filesystem.Dmitry Khalanskiy [JB]
04/29/2025, 5:56 PMFileSystem
explicitly. The same goes for clocks and for TimeZone.currentSystemDefault()
For timezone databases, I can't imagine any use cases where the timezone database wouldn't be the only one in the program. I am not talking about not having the option to define a custom tzdb at all, it's just not treating it as a singleton in the program that feels excessively verbose to me.Dmitry Khalanskiy [JB]
04/29/2025, 5:57 PMEquality of two timezones should likely also delegate to equality of the resolverWhich either involves comparing the content of the resolvers (that is, all timezone information) or using the identity comparison and breaking
by
-delegation, right?jw
04/29/2025, 5:59 PMby
if you explicitly create your own tz instances using that new zonedclock instance. If you are just delegating, the creator of the actual tz instance is still the original zonedclock instance.kevin.cianfarini
04/29/2025, 6:00 PMTimeZoneSerializer
. An API which encourages IoC also categorically disqualifies that kind of thing which has benefits (like not trying to load an identifier implicitly which your system might not have, even if you don’t want to use it in any meaningful way other than as an identifier).
Of course this offers the opportunity for someone to have multiple tzdata sources in a single program but I’d expect that to be really uncommon whereas something like deserializing a TimeZone is incredibly common and error prone. Feels like the API should naturally guide people towards the thing that’s correct, and not guide people away from the thing that’s bad if that requires a bunch of other sacrifices (like static IO)kevin.cianfarini
04/29/2025, 6:04 PMIf the given object is not a Path, or is a Path associated with a different, then this method returnsFileSystem
.false
Whether or not two path are equal depends on the file system implementation. In some cases the paths are compared without regard to case, and others are case sensitive. This method does not access the file system and the file is not required to exist. Where required, themethod may be used to check if two paths locate the same file.isSameFile
kevin.cianfarini
04/29/2025, 6:05 PMTimeZone.equals
simply checks the tzdb id and that the implicit resolvers are identity equivalent, and another more complicated function like TimeZone.matchesExactly(other: TimeZone)
uses the implicit resolver to determine if two timezones have the exact same rules. This will be a much less common operation than normal equals.Dmitry Khalanskiy [JB]
04/29/2025, 6:12 PMAPI shouldn’t encourage IO via static dependenciesI fail to see the alternative. Ok, let's say we make it as cumbersome as possible to serialize a
TimeZone
. What does a programmer do after realizing this is difficult to do? Give up and ask the product manager to remove the task that would involve transferring a TimeZone
over the network? I doubt it. Read about the potential issues, swear a bit, and rewrite the program to support dependency injection to achieve exactly the same result? Maybe. Send a UtcOffset
instead of a TimeZone
, because it's easy to acquire and (de)serialize? Quite possible!
In none of the cases did the programmer actually gain anything. In the worst case, they fall back to UtcOffset
, which some datetime libraries call "timezone information" to this day, by the way. Am I missing some happy scenario?
> This will be a much less common operation than normal equals.
You said it. Having everyone learn about fine intricacies without a real need is what we should avoid in API design.
Alright, I did get some interesting insights from the discussion, thank you! I have to leave for now. I'll check in tomorrow.kevin.cianfarini
04/29/2025, 6:13 PMI fail to see the alternative. Ok, let’s say we make it as cumbersome as possible to serialize aThey could just access the. What does a programmer do after realizing this is difficult to do? Give up and ask the product manager to remove the task that would involve transferring aTimeZone
over the network? I doubt it. Read about the potential issues, swear a bit, and rewrite the program to support dependency injection to achieve exactly the same result? Maybe. Send aTimeZone
instead of aUtcOffset
, because it’s easy to acquire and (de)serialize? Quite possible!TimeZone
TimeZone.id
which is the identifier and not the service part of a timezone.kevin.cianfarini
04/29/2025, 6:14 PM@Serializable
class SomeClass(val tz: TimeZone)
They’d have
@Serializable
class SomeClass(val tzdbId: String)
and populate that with
SomeClass(someTimeZone.id)
jw
04/29/2025, 6:20 PMjw
04/29/2025, 6:22 PMkevin.cianfarini
04/29/2025, 6:22 PMYou said it. Having everyone learn about fine intricacies without a real need is what we should avoid in API design.I don’t feel like this is a fair argument. Right now people need to know about the fine intricacies of their platforms tzdata simply to deserialize a timezone. We’ve just shifted the problem to a much more common use case. I promise I’m trying to be nice and not condescending in this thread. I appreciate you indulging my curiosity here. I felt like temperatures rose a little bit towards the end which I didn’t mean to cause, but if I did I apologize. It felt like my comments were being taken as personal attacks and I didn’t intend them to be. I hope we can continue talking about the merits and demerits of each API shape in a professional manner.
jessewilson
04/29/2025, 6:27 PMEurope/Kyiv
. Only because String
is super loose, and I can easily accidentally swap the strings for the emailAddress
and timeZoneId
.jessewilson
04/29/2025, 6:33 PMkevin.cianfarini
04/29/2025, 6:35 PMjessewilson
04/29/2025, 6:38 PM@Burst
class DepositSchedulerTest(
val tzdbPath : Path = burstValues("/zoneinfo-2025.1".toPath(), "/zoneinfo-2025.2".toPath())
) {
@Test
fun testDepositScheduler() {
val tzdb = loadTzdb(tzdbPath)
...
}
}
jessewilson
04/29/2025, 6:41 PMjw
04/29/2025, 7:17 PMjessewilson
04/29/2025, 7:27 PMkevin.cianfarini
04/29/2025, 7:27 PMTimeZoneSerializer
for similar reasons?jessewilson
04/29/2025, 7:27 PMjessewilson
04/29/2025, 7:28 PMDmitry Khalanskiy [JB]
04/30/2025, 7:24 AMThey could just access theAh, so that's what you consider error-prone? I missed this point, sorry. For me, the main problem with serializingwhich is the identifier and not the service part of a timezone.TimeZone.id
TimeZone
isn't that they get constructed whenever they get deserialized (which may fail, but rarely) but that querying a timezone on different computers may lead to different results (which is much more likely, given how often timezone databases change). This doesn't get solved by using id
, but then again, this doesn't really get solved at all without careful management of the timezone database on all computers involved.
Are there other kotlinx-datetime maintainers?Sure. I pinged the team to see if they would like to join the discussion.
If the library was designed with timezone inversion of control today, do you think your arguments against introducing it would be strong enough to convince yourself to change away from it?If we indeed published something with timezone inversion of control, this would mean we've arrived at an API worth publishing, so probably not. What I doubt is the existence of such an API.
I don’t feel like this is a fair argument.Since I was getting ready to leave, I've described my point more succinctly that I should have, hoping it would still get across, sorry. What I mean is: what we are discussing here is not relevant for most developers. As https://github.com/Kotlin/kotlinx-datetime/blob/master/README.md#design-overview says,
It is not all-encompassing and lacks some domain-specific utilities that special-purpose applications might need. We chose convenience over generality, so the API surface this library provides is as minimal as possible to meet the use-cases.With this design goal, we can not afford to inconvenience developers without a good reason. https://github.com/Kotlin/kotlinx-datetime/issues/17 is what I think is a good reason: indeed, you may want to test your code with a specific
TimeZone
as currentSystemDefault()
, and the inconvenience of writing something like SystemClock.currentSystemDefaultTimeZone()
instead of TimeZone.currentSystemDefault()
is slight. Having people learn additional API just to choose how they want to compare two time zones is a tough sell and should have a good reason besides conceptual purity and universality (which is explicitly a non-goal). Is there one?
Right now people need to know about the fine intricacies of their platforms tzdata simply to deserialize a timezone.That's true. Is there anything we can do about it besides introducing
kotlinx-datetime-tzdata
to streamline using the most up-to-date timezone information in Kotlin projects?
I felt like temperatures rose a little bit towards the endI haven't felt anything of the sort, so everything's fine on my side, no worries! If temperatures did indeed rise, I am sorry that I've probably contributed (representing one of the sides of the discussion completely), I'll try being more careful. On my part, I was trying to 1. Convey the inherent tension between the use cases involving just a single timezone database and expecting it to be convenient and the new conceptual difficulties that arise whenever there are several timezone databases. Given how much more rarely I'd expect several timezone databases to be needed than, say, several filesystems or several default system timezones, I find it hard to justify the added complexity. 2. See if this discussion produces some fresh ideas regarding the most nasty bits of the added complexity.
Only becauseIf you intend to take this approach, wouldn't you then have something like this in your program anyway?is super loose, and I can easily accidentally swap the strings for theString
andemailAddress
.timeZoneId
data class Email(val value: String)
data class Name(val value: String)
data class Surname(val value: String)
// data class TimeZoneId(val value: String)
If you would, what is the issue with having one more class for a timezone identifier? What would you gain from us being the ones providing this class?
I would like the option to validate my code before and after this change with a JUnit test.This is interesting and is an actual use case for having several timezone databases! With a one-timezone-per-program
ServiceLoader
-like approach, this would only be possible by having the tests for different timezone databases in separate source sets. This would still be possible, but indeed cumbersome.
But I think it's telling that all the serious, closed-source projects I work on deny-list APIs like the static timezone in favor of ones on a custom clock to provide that inversion of control.Could you clarify what exactly is banned? Just
currentSystemDefault()
(whose being static I agree is a problem), or somehow, using TimeZone
with the system database? If it's the latter, what are you using as the alternative?CLOVIS
04/30/2025, 7:46 AMpersist a timezone identifier and then have a large geopolitical event. you should still be able to parse our that identifier just like the non-existent path, and then fail to resolve it.Can we clarify what everyone expects from the API, here? I get a feeling that not everyone agrees on what the ideal version should look like. My understanding is;
@JvmInline value class TimeZoneId(val id: String) {
init {
// Checks whether 'id' has the correct grammar/shape, but doesn't check if it exists
}
}
// ↑ this one is meant to be serialized
// it's a pure identifier with no behavior, but is still type-safe 'enough': it can't be an email address or a phone number
// ↓ this one isn't meant to be serialized
class TimeZone … // unchanged from the current library, except the removal of 'TimeZone.of'
fun interface TimeZoneResolver {
fun resolve(id: TimeZoneId): TimeZone
companion object {
val System: TimeZoneResolver // uses the existing implementation
}
}
// ↑ the 'service' part
// may be part of a new Clock type, or may be its dedicated interface, TBD
Looking at the examples provided in previous messages; about the DTO use-case with Europe/Kyiv:
// Before
data class Foo(
val name: String,
val foo: Int,
val timeZone: TimeZone,
val at: LocalDateTime,
)
val Foo.instant get() =
at.toInstant(timeZone)
// After
data class Foo(
val name: String,
val foo: Int,
val timeZone: TimeZoneId,
)
context(resolver: TimeZoneResolver)
val Foo.instant get() =
at.toInstant(resolver(timeZone))
Thankfully context parameters make this much simpler (otherwise you'd have to convert it to a function to be able to specify additional parameters). The API seems fairly clear to me, and it seems to solve the initial issue. This solution would require implementing caching (if there's none already) because users will resolve the same ID multiple times (whereas previously the DTO contained the resolved class), but that's already the case for many other objects so it's probably not a big issue?
Is that correct?Dmitry Khalanskiy [JB]
04/30/2025, 7:48 AM// Checks whether 'id' has the correct grammar/shape, but doesn't check if it existsSee https://kotlinlang.slack.com/archives/C01923PC6A0/p1745943826890139?thread_ts=1745939940.516159&cid=C01923PC6A0: I don't know of a way to implement this check.
sandwwraith
04/30/2025, 10:27 AMTimeZoneResolver
or ZonedClock
service akin to FileSystem
, most code would still end up with Resolver.Default.resolve(id)
— provided that Resolver.Default
tzdb can be updated dynamically — which is not that far away from TimeZone.of(id)
— because I don't know many meaningful ways to use more than one tz provider. However, such API surely would help with a) testing tz migrations (as in your example) b) highlighting that we use implicit system configuration resource. So I still lean to the fact that ZonedClock/Resolver
approach is better.Dmitry Khalanskiy [JB]
04/30/2025, 10:56 AMfun Config.obtainDefaultLoggingTimeZone(): TimeZone
. I think it's entirely plausible that someone will write this, no matter what approach we take. Do you agree?
If we allow the flexibility of many tzdbs per application, a question arises: what timezone database is this TimeZone
from? Did the author of obtainDefaultLoggingTimeZone
query the default timezone database, or is it taken from somewhere else? Can I put it into my existing val timeZoneCache: Map<String, TimeZone>
that only keeps track of timezones I use, or will it overwrite a different existing entry?
For filesystems, all these questions are unavoidable and even welcome: if you have a file from some filesystem, you do need to know what filesystem that is. For timezone databases, I don't understand the value of having writers of client code look for answers to these questions instead of treating a TimeZone
as a self-contained thing that's always interpreted by a service-loaded database. The extra details that come from highlighting the existence of more than one filesystem help bring clarity, but how do the extra details help in our case? Are we improving the clarity or just introducing new failure modes for the sake of adhering to the "functional core, imperative shell" architecture?
In other words: if you have two timezone databases in your production code, one of which is more recent than the other, I believe that it is always a bug. On the other hand, it is sometimes expected that you will have more than one filesystem.sandwwraith
04/30/2025, 11:00 AMClock
may be a counter-example to your argument. Any reasonable production code uses only Clock.System
, and produced `Instant`s do not have any association with the clocks. Yet, we pass Clock
instance everywhere for the sake of testability and IoC — in test code we may want to use any kind of clocks we like.
Same with `TimeZoneResolver`/`TimeZoneResolver.SystemDefault`.Dmitry Khalanskiy [JB]
04/30/2025, 11:09 AMkotlinx-datetime
, we don't use a custom timezone database to check our implementation against. Instead, we query the historical data, which can no longer change. A 2008 DST transition will always be there.Dmitry Khalanskiy [JB]
04/30/2025, 11:18 AMClock
values is not important to the program logic, as it's not distinguishable from the system clock behaving erratically (after being set by the user/adjusted by the NTP). Using the wrong system clock may or may not be a bug, but having `Instant`s interact without considering their source Clock
does not introduce any new problematic behaviors that wouldn't be observed otherwise.jessewilson
04/30/2025, 11:22 AMjessewilson
04/30/2025, 11:23 AMTimeZoneResolver.System
jessewilson
04/30/2025, 11:23 AMJsJodaTimeZoneResolver
Dmitry Khalanskiy [JB]
04/30/2025, 11:24 AMjessewilson
04/30/2025, 11:27 AMjessewilson
04/30/2025, 11:28 AMjessewilson
04/30/2025, 11:29 AMjessewilson
04/30/2025, 11:34 AMjessewilson
04/30/2025, 11:35 AMDmitry Khalanskiy [JB]
04/30/2025, 12:02 PMinterface TimeZoneDatabase { fun queryTimeZone(id: String): TimeZone; val priority: Int }
• Implementations of this interface are loaded on all platforms via the serviceloader mechanism. The current implementations (querying the system timezone) are used as fallbacks with the lowest priority by default.
• kotlinx-datetime-zoneinfo
is provided for all targets with the latest timezone database, possibly in several forms, and has a low priority.
• Anyone can write their own implementation and have whatever behavior they want, but the interface is strictly intended for service loading, not for instantiating inside the program, and its contract does not support mix-and-match behavior. fun queryTimeZone
is marked with an opt-in annotation, because it is supposed to be called from inside TimeZone.of
, not in user code.
• It is possible to load a separate timezone database implementation in tests, but only one at a time.
The behavior of the system changes with adding new dependencies, and there's no advertised way to use several timezone databases in one program. However, the use case of defining one's own timezone database with arbitrary data sources (the network, the filesystem, etc.) is covered.kevin.cianfarini
04/30/2025, 12:02 PMClock.System
can easily be tampered with. The IoC of Clock though allows us to implement something with a more trusted time source, like Google's trusted time API.
These examples feel near identical to me since there's only one correct answer for what Clock.now
produces, and the results from Clock.System
and Clock.TrustedTime
may not agree within the context of a single program, just like with having multiple tzdata loaded.Dmitry Khalanskiy [JB]
04/30/2025, 12:12 PMkotlin.time.Clock
.
If you receive a TimeZone
from some component written with an outdated timezone database in mind, you typically want to reinterpret the value you receive using the most up-to-date timezone database, and because the timezone has an identifier, this is possible.jessewilson
04/30/2025, 12:14 PMCLOVIS
04/30/2025, 12:14 PMIt is possible to load a separate timezone database implementation in tests, but only one at a time.All our tests run in parallel using coroutines. This has the potential to break a lot of things.
jessewilson
04/30/2025, 12:22 PMDmitry Khalanskiy [JB]
04/30/2025, 12:25 PMkevin.cianfarini
04/30/2025, 12:40 PMfun queryTimeZone
is marked with an opt-in annotation, because it is supposed to be called from inside TimeZone.of
, not in user code.
Removing explicitness from this means anyone can publish a library which has a rogue TimeZoneDatabase
which will be service loaded without your knowledge, potentially maliciously. I don’t like that other libraries would have the ability to implicitly change what timezones are resolved when I write TimeZone.of("America/New_York")
. Being explicit with IoC avoids this problem.kevin.cianfarini
04/30/2025, 12:48 PMString
or a wrapper like TimeZoneId
), and a service (the TimeZone
).jw
04/30/2025, 4:44 PMjw
04/30/2025, 4:51 PM