Fix: fall back to UTC for timezones a service cannot resolve - #248
Merged
Conversation
alies-dev
force-pushed
the
fix/offset-style-timezones
branch
from
August 20, 2026 15:33
92f72eb to
6b252b1
Compare
Collaborator
Author
|
Merge-order note: #249 adds |
alies-dev
force-pushed
the
fix/offset-style-timezones
branch
from
August 20, 2026 15:49
d742dbe to
05d8e7d
Compare
An offset (`+02:00`), an abbreviation (`CEST`) and the `Etc/GMT±N` family are all names DateTimeZone accepts and no calendar service can resolve. Both generators need the same answer, so the check lives on Link and judges the two ends together.
Google ignores a ctz, stz or etz that is not a zone it knows, so the local times in `dates` were left to be read in the viewer's own zone and the event landed at the wrong instant for everyone else. Those events are now written as UTC instants with no zone parameter. Corrects the comment claiming Google handles `Etc/GMT+5` unencoded: it is rejected raw and percent encoded alike.
RFC 5545 forbids an unquoted `:` in a param-value, so `TZID=+02:00` cut `DTSTART` in half and left a value no parser could read. The distinct timezone path now only names the zones when both of them resolve, and writes UTC endpoints otherwise.
The `str_contains($name, '/')` gate was standing in for two separate things and got one of them wrong. `Japan`, `GB`, `Singapore`, `Eire`, `W-SU` and the rest of the slashless backward names are aliases of a region and resolve exactly like `US/Pacific`, which the same code kept. They were being downgraded to UTC, which held the instant but dropped the zone the caller named, silently. The TZDB list is now the only positive test. The entries that stand for no place are excluded by name instead: the POSIX rule sets, the other spellings of UTC, the `Etc/` tree and `Factory`. Also corrects the README, which called `Etc/GMT+5` unresolvable. It is a real IANA identifier; the reasons to turn it down are that Google rejects it and that its sign runs the opposite way from the offset it names.
`Etc/UTC` goes with the rest of the tree, and because both ends are judged together, refusing it drops the other end's zone as well. Naming `UTC` instead keeps a UTC endpoint paired with a named zone.
An event from `UTC` to `Etc/UTC` is two spellings of one zone, so it collapses and only the start zone is ever emitted. Judging both ends unconditionally let the discarded end zone refuse the event and drop it to the UTC fallback, costing the start zone the name it was going to be written under. The distinct path still needs both ends, since naming one and not the other leaves half the event pinned to a zone and half of it loose.
alies-dev
force-pushed
the
fix/offset-style-timezones
branch
from
August 20, 2026 16:16
0673b19 to
f825338
Compare
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context and Purposes
A
DateTimeZonedoes not have to name a place. Parsing any ISO 8601 string with an offset gives you one that does not:The generators passed that name straight through, and both services broke on it.
Google received
&dates=20260101T100000/20260101T110000&ctz=+02:00. Live testing against calendar.google.com showsctz=+02:00is ignored in every encoding, so the wall clock pair indateswas reinterpreted in the viewer's own timezone. The event silently landed at the wrong absolute time for every viewer outside +02:00, and thestz/etzpair on the distinct timezone path failed the same way. The same testing disproved the comment inGoogle.phpclaiming Google handles unencoded names such asEtc/GMT+5: that family is rejected both raw and fully percent encoded, so no encoding makes it work.ICS received
DTSTART;TZID=+02:00:20260101T100000. RFC 5545 defines a param-value as text that cannot carry an unquoted:(section 3.1), so a conforming parser reads the parameter asTZID=+02and takes00:20260101T100000as the property value, which is not a valid DATE-TIME. The event was corrupt in every client, and negative offsets failed identically. A TZID also has to name a zone the client can look up (section 3.2.19), which an offset never does.Both are the same underlying question, so the answer lives in one place.
Link::hasResolvableTimezones()reports whether both ends carry a name a calendar service can resolve, and both generators ask it:Europe/Amsterdam, including the backward names such asUS/Pacific) resolves, and so doesUTC;+02:00) or an abbreviation (CEST) names no region and carries no daylight saving rules;Etc/GMT±Nis a real TZDB identifier, but its sign is inverted from the offset it names and Google rejects it outright;EST,MST7MDT) are POSIX rules rather than places.When the answer is no, the generators write the unambiguous UTC form instead. Google gets
dates=20260101T080000Z/20260101T090000Zand noctz,stzoretz. The ics file gets UTCDTSTART/DTENDand noTZID. The instant is preserved in both, which is what was being lost.Both ends are judged together. Naming only the departure of a flight whose arrival zone is an offset would leave the two halves of one event on different footings, so an unresolvable zone at either end sends the pair to UTC.
An all-day event is the one case that is not converted. Its endpoints are calendar dates rather than instants, and rewriting them in UTC would shift a midnight in
+02:00back onto the previous day. Those dates stay as they were given and only thectznaming is dropped.Nothing changes for an event that already names a region: every existing snapshot is untouched.
Fixes #234