I’m wondering if any of the kotlinx-datetime maint...
# kotlinx-datetime
k
I’m wondering if any of the kotlinx-datetime maintainers have read Jesse’s blog post titled Identifiers aren’t services in which he argues in part that
kotlinx.datetime.TimeZone
shouldn’t query the underlying database from the tzdb entry when calling
TimeZone.of(String)
. Do you have thoughts?
This also seems somewhat related to this Github issue.
If any of the maintainers are willing to entertain my thoughts for a second, maybe something like this wouldn’t be too cumbersome and dovetails nicely with another Github issue.
Copy code
public interface ZonedClock : Clock {
  public fun now(): Instant 
  public fun currentTimeZone(): TimeZone 
  public fun queryTimeZone(id: TimeZoneId): TimeZone
}
I imagine the annoying problem to overcome with an API like with would be deserializing timezones and loading their offset rules from String identifiers in a network response.
c
I think this goes against Make invalid states unrepresentable. If I create a
TimeZone
, 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
.
[yes this article is about type-level validation, which is better than the current construction-level validation, but it's still better than no validation at all]
I'm not sure what the use for a
TimeZone
class that doesn't validate its content is. How is it different from a
String
?
k
Couple of things: • The
TimeZone
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).
I’m just sort of spitballing here. I think a design like this could also make it somewhat nicer to load custom timezone databases both in test and in production as well without having to rely on swapping updating tzdata for a JVM, for example.
j
A url is similar
1
Imagine if you couldn't construct a URL because your machine couldn't connect to it
2
yes black 1
k
The motivation for this topic came from me having to use Java’s stdlib recently and the amount they rely on implicit static dependencies for things like the Filesystem and tzdata is not great. In many places Kotlin’s made a clean break from this, and I’m wondering if we can do it in kotlinx-datetime too
j
Not being able to parse a well-formed ID and not being able to resolve the database information for an ID are two separate things.
👌 1
c
@jw the difference is whether IO is involved. I definitely don't want to have hidden IO, but the tzdata is bundled into the JVM/whatever, right? It's basically an internal resource of the program, same as any validation code. The fact that it's actually a file is irrelevant, it could be rewritten to Kotlin and the behavior would be the same.
k
> the difference is whether IO is involved. I definitely don’t want to have hidden IO, but the tzdata is bundled into the JVM/whatever, right? As far as I can tell, kotlinx-datetime does IO when resolving the offset rules and names for a timezone. https://github.com/Kotlin/kotlinx-datetime/blob/master/core/linux/src/internal/TimeZoneNative.kt#L24-L45
c
In 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).
k
kotlinx-io is a much better abstraction building on top of Okio, for example.
SystemFileSystem
being a requirement, and not something you can avoid with static function calls.
kotlinx-datetime discouraging
Instant.now()
is another example in favor of
Clock
is another example.
c
> As far as I can tell, kotlinx-datetime does IO when resolving the offset rules for a timezone. https://github.com/Kotlin/kotlinx-datetime/blob/master/core/linux/src/internal/TimeZoneNative.kt#L24-L45 This is the Kotlin Native linux implementation, which of course uses the system tzdata. It would be very weird if
TimeZone
could contain an invalid timezone, no? Still, looking up a system configuration file is nothing like a network request.
j
The kotlin stdlib extensions (which I'm guessing that you are referring to) do not alter the implicit or explicitness of java.nio usage. Once you have a
Path
, the file system is carried along.
You can still never get an invalid
TimeZone
though
k
> This is the Kotlin Native linux implementation Yeah but it does IO, which is the question you asked.
c
Parallel question: the JVM can do IO on
new
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)
j
Wholly unrelated
1
k
My opinion is that I think we should focus on kotlinx-datetime. We aren’t going to solve every single JVM problem in this thread.
c
Sure, but I'm trying to find other examples to understand your point better.
k
I think
Clock
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
.
The blog post in the OP has several examples, too.
c
Once you have a
Path
, the file system is carried along.
But that's what the article is against, no? The article would want a file-system-agnostic
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?
j
you were claiming Kotlin's extensions somehow aided the implicitness. i am merely pointing out that is not true.
👍 1
the filesystem is still a good equivalent, as you can create the identifier of a file arbitrarily but then resolve that against the actual service to get access to the real file. that file may disappear after your access, or have not appeared yet. delete a project from your file system and then open it as a recent project in IntelliJ. the IDE still persists and likely parses that into a path representation and then asks the filesystem to resolve it. persist 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. persist a url and then pull your ethernet. you should still be able to parse our that URL just like the non-existent path and tz id, and then fail to resolve it.
a path would reject malformed bytes but not well-formed ones to a non-existent file. a tzid would reject a malformed string but not well-formed ones to a non-existent tz. a url would reject malformed data but not well-formed ones to a non-existent server. and so on.
k
But that’s what the article is against, no? The article would want a file-system-agnostic
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.
This is the design of both Okio and kotlinx-io, just for reference.
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?
You’re conflating what I recommended above —
TimeZone
(the thing which has offset rules) would be separate from any sort of IANA identifier.
d
I haven't read the article, but judging by the name, I think I know the idea it's trying to convey. File paths and URLs are completely different from timezone identifiers in that there are meaningful pure operations you can make on paths and URLs, but the only operation on a timezone identifier that you can do without having access to a timezone database is (theoretically!) to attempt to parse it. I don't think we can do even that, though. See https://data.iana.org/time-zones/tzdb-2022b/theory.html Instead of a specific grammar, it has "the general guidelines used for choosing timezone names, in decreasing order of importance", that have also changed over time. So,
TimeZoneId
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.
k
Sure, let’s consider then the following API shape then:
Copy code
public 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.
I need to build a report that summarizes the emails that our service sends each
day. The input is a set of
SentEmail
records:
```data class SentEmail(
val customerId: Id,
val timeZone: TimeZone,
val locale: Locale,
val emailAddressId: Id,
val templateId: TemplateId,
val enqueuedAt: Instant,
val deliveredAt: Instant,
)```
The
TimeZone
class looks like an innocuous way to track a string like
“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
Europe/Kyiv
(renamed from
Europe/Kiev
in 2022).
```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,
TimeZone
always loads the offset
rules.
this is the entire section on TimeZone fwiw. It’s short.
d
at 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.
k
I don’t think that’s entirely clear based on the KDoc for
TimeZoneSerializer
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?
d
Good point. We could indeed improve the documentation here, it doesn't properly describe the failure modes. As for considering timezone identifier APIs that do not access the platform's timezone database: I don't understand what that could even meaningfully be if not
typealias TimeZoneId = String
. Why introduce code with no behavior?
k
I think one of my food-for-thought API examples got lost in the thread. Let me repaste while also considering that any value type for an IANA TZ identifier is really just a String.
Copy code
public 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.
(I’m not sold on querying for time zones living in a clock, but just consider it as an example)
A change like this has reverberations across the ecosystem, too. Like — does a
object 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.
j
Or that you probably shouldn't be using
TimeZone
in your serialization models in the first place.
1
👌 1
k
I’m bringing this up because kotlinx-datetime is not yet stable and it’d be great to resolve some of the warts from java time before we hit 1.0 😅
d
This resembles a plan we've considered a couple of years ago, but we encountered several conceptual issues that eventually stopped us from going through with it. The crux of the issue is that realistically, there's going to be at most one timezone database per any software system. If you have a timezone identifier whose information you need to query, you just want the most accurate, up-to-date information about it, you don't ever need to, say, find out what a
LocalDateTime
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 using
TimeZone
in your serialization models
Serialization 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.
k
Would it be possible to open up that investigation again but publicly in a Github issue or Github discussion? I’d like to chime in on this more formally than a Slack thread. I suspect others would, too. > Serialization doesn’t only happen across network boundaries. I think we should consider that an extremely large use case for serialization is explicitly to pass data over the network where this TimeZone serialization is problematic. I don’t feel like this is something which should be easily ignored. I feel like this abstraction helps in several ways, too: 1. You don’t run into the network serialization issue. 2. This plays naturally with a potential future
ZonedClock.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.
j
The JVM also currently requires a restart, which can be annoying
k
It’s completely unclear to me how we’d update tzdata on native platforms, so having a simple dependency that owns that complexity would be awesome.
j
Gotta update the OS-level ones
👍 1
😬 1
d
Would it be possible to open up that investigation again but publicly
Here's a puzzle since you're interested to demonstrate how tricky this issue is:
Copy code
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 annoying
Could you elaborate? Does someone run their servers for months at a time and wants to be able update the timezone database without stopping them?
k
1. I think you’d want to include information about where the tz was sourced from, eg.
SYSTEM[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.
j
The length of time the JVM is running is mostly irrelevant. It's tying the update process to the restart that's the problem. We don't have to do this for certs, configuration, routing rules, etc.
🙏 1
d
I think you’d want to include information about where the tz was sourced from
Yes. 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 querying
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.
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?
j
toString() seems fine
k
> Yes. 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). I don’t agree with either of these points. A TimeZone could store the following information:
Copy code
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.
j
The current design feels a lot like java.io.File, where it's good until it isn't. I fully expect my production application to interact with the system file system most of the time, but sometimes it needs to read from a zip, or in memory for tests. And the problem is rarely my system, but a library that assumes it can operate on a
File
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.
👍 1
👍🏻 1
yes black 1
k
Another option would be to include the service object which does tz querying inside the actual timezone.
Copy code
class 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.
That’s kind of like
java.nio.Path
which will include an implicit FileSystem in it based on how that path is acquired.
The difference here would be that kotlinx-datetime wouldn’t offer a static way of acquiring a tz like
Paths.get
which has an implicit dependency on the system’s filesystem.
d
For filesystems, I completely support this argument and am fully in favor of passing a
FileSystem
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.
Equality of two timezones should likely also delegate to equality of the resolver
Which either involves comparing the content of the resolvers (that is, all timezone information) or using the identity comparison and breaking
by
-delegation, right?
j
It only breaks
by
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.
k
> 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. I think what we’re trying to argue isn’t that you should have multiple tzdata loaded into a single program, but rather that the API shouldn’t encourage IO via static dependencies that isn’t configurable like seen in
TimeZoneSerializer
. 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)
Also worth considering the contract of JDK Path.equals because it’s a similar use case.
If the given object is not a Path, or is a Path associated with a different
FileSystem
, then this method returns
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, the
isSameFile
method may be used to check if two paths locate the same file.
So it might be that
TimeZone.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.
d
API shouldn’t encourage IO via static dependencies
I 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.
k
I 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!
They could just access the
TimeZone.id
which is the identifier and not the service part of a timezone.
So instead of having
Copy code
@Serializable
class SomeClass(val tz: TimeZone)
They’d have
Copy code
@Serializable
class SomeClass(val tzdbId: String)
and populate that with
Copy code
SomeClass(someTimeZone.id)
j
Are there other kotlinx-datetime maintainers?
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?
k
You 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.
j
IDS PLEASE I would really like a type-safe identifier that wraps a String containing an Olson name like
Europe/Kyiv
. Only because
String
is super loose, and I can easily accidentally swap the strings for the
emailAddress
and
timeZoneId
.
DON’T FORCE ME TO ONLY USE THE HOST COMPUTER’S TZDB, PLEASE There’s political interest in changing America/New_York to make DST permanent. I would like the option to validate my code before and after this change with a JUnit test. If the tzdb is polymorphic, I can do that.
k
> DON’T FORCE ME TO ONLY USE THE HOST COMPUTER’S TZDB, PLEASE Jesse, can you clarify? Are you saying you have a use case where you’d want two separate tzdata sources loaded simultaneously in a single program for unit testing purposes?
j
Yeah, I’d love to do this:
Copy code
@Burst
class DepositSchedulerTest(
  val tzdbPath : Path = burstValues("/zoneinfo-2025.1".toPath(), "/zoneinfo-2025.2".toPath())
) {
  @Test
  fun testDepositScheduler() {
    val tzdb = loadTzdb(tzdbPath)
    ...
  }
}
👍 1
We do scheduled stuff like stock buys and paycheck deposits, and if the time zones are changing we want to be pretty rigorous about it. It’s bad news if one system thinks it’s 9am in NYC and another thinks it’s 8am.
j
I have a lot of public projects on GitHub that are 3-10 files using kotlinx-datetime and it works great. 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.
j
I’d like my Wasm-in-the-browser app to use use my company’s web service TZDB
🤯 1
k
Just out of curiosity, in CashApp or any of its related back end services, do y'all deny list the
TimeZoneSerializer
for similar reasons?
j
We don’t use enough kotlinx.serialization to have needed that yet
Really we would want to denylist using a TimeZone in a data class if it’s a service and not an identifier
👍 1
d
They could just access the
TimeZone.id
which is the identifier and not the service part of a timezone.
Ah, so that's what you consider error-prone? I missed this point, sorry. For me, the main problem with serializing
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 end
I 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 because
String
is super loose, and I can easily accidentally swap the strings for the
emailAddress
and
timeZoneId
.
If you intend to take this approach, wouldn't you then have something like this in your program anyway?
Copy code
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?
blob ty sign 1
c
persist 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;
Copy code
@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:
Copy code
// 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?
2
😻 2
d
// Checks whether 'id' has the correct grammar/shape, but doesn't check if it exists
See https://kotlinlang.slack.com/archives/C01923PC6A0/p1745943826890139?thread_ts=1745939940.516159&amp;cid=C01923PC6A0: I don't know of a way to implement this check.
😯 1
s
My five cents here: I agree that we have to implement a way to update tzdb without restarting whole application/JVM, so some kind of service loading is required. I also think using `String`s instead of `TimeZone`s in serializable classes is a better idea (if you want fail-safe for older devices with older tzdbs), because as Dmitriy said, there's no really other way to validate timezone except looking it up in tzdb. For the whole construction/inversion of control debate: I think even if we provide some
TimeZoneResolver
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.
d
My main issue is with b), as I consider it a downside and not an upside. Imagine a third-party library (or just a piece of client code deep in the codebase) that has a function like
fun 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.
s
I guess
Clock
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`.
2
d
Injecting different `Clock`s for testing is quite useful. A clock's state varies over time in the span of your program's operation. There are plenty of things you may want to check with a fake clock: does your program react well to the system time getting adjusted? What exactly does it return at some specific moment in time? How does your program react if one component has detected a later clock value than the other? To check how your code deals with DST transitions, you don't need synthetic data. In
kotlinx-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.
A more important point: an interaction between different
Clock
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.
j
Doing a TimeZoneResolver interface might make the js-joda dependency less arbitrary?
On platforms that have built-in data, I’d use
TimeZoneResolver.System
On WasmWasi that doesn’t, I’d use
JsJodaTimeZoneResolver
d
Maybe you mean Wasm/JS? Wasm/WASI uses our own `kotlinx-datetime-zoneinfo`: https://github.com/Kotlin/kotlinx-datetime?tab=readme-ov-file#note-about-time-zones-in-wasmwasi
j
Ahhh, yep
Rather than having the built-in resolver’s behaviour change when I add a new dependency, I just pick a full-featured resolver
1
And if I’m trying to reduce the size of a Wasm app that needs only America/New_York, I can do that by packaging my UnitedStatesOnlyTimeZoneResolver ?
This is theoretical! I don’t need this now! But it’d be nice to give developers an interface we can implement if we want something other than All and Nothing
d
Just that so we're on the same page, let me explicitly describe my vision of how the timezone database should be provided (eventually, when the facilities for this exist): • There is an interface like
interface 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.
k
> 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. > I feel like this same argent applies for Clocks, too. Really there's only one valid Instant in time per nanosecond, but
Clock.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.
1
d
These examples are very distinct. The main difference is that realistically, you can't do anything about an incorrect clock reading. You have some data from the clock, and whether or not you know what the clock was, you have no choice but to treat it as the truth. In theory, you can attempt to determine how several clocks running in parallel differ and adjust the time taking that into account, but I would be very surprised to learn that someone does that with
kotlin.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.
j
Should that component return a TimeZone or a TimeZoneId? I think both choices are possible and might even make sense in different contexts?
👍 1
c
It 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.
j
I am pretty happy with different FileSystem implementations in Okio. The system file system, the resources one, and zip file systems all coexist and it’s … boring? I think time zone DBs could be similar. There’s a system one that we use almost exclusively, and a handful of other implementations for special cases: the system one is incomplete or stale, or we’re unit testing a zone change.
d
If a library uses the system database, then stale or not, you're stuck with what it returns if you don't manually reinterpret it.
k
> 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. 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.
> If a library uses the system database, then stale or not, you’re stuck with what it returns if you don’t manually reinterpret it. I think this speaks to the desire to have a separate identifier (either a
String
or a wrapper like
TimeZoneId
), and a service (the
TimeZone
).
j
Service loaders are such a hassle. It moves configuration of something from code into ops/build.
1
We denylist Dispatchers.Main entirely but suffer because of its use in libraries. Usually they can be convinced to hoist the context parameter to their entry point. We remain free of setPain setMain, thankfully.