Skip to content

Better preserve timezones in ical - #569

Open
sergioisidoro wants to merge 6 commits into
ice-cube-ruby:masterfrom
sergioisidoro:timezone-fixes
Open

Better preserve timezones in ical#569
sergioisidoro wants to merge 6 commits into
ice-cube-ruby:masterfrom
sergioisidoro:timezone-fixes

Conversation

@sergioisidoro

@sergioisidoro sergioisidoro commented Jun 17, 2025

Copy link
Copy Markdown

Trying to approach #568

May also fix:
#535


Claude summary

The commit Harden iCal TZID handling follows up on the original TZID support in this PR, after auditing the iCal round-trip for issues of the same class. Each item below was reproduced before fixing and is covered by a spec.

Parser

Dropped the hard ActiveSupport dependency. The TZID path called String#in_time_zone, so any iCal string containing a TZID= raised NoMethodError for users without ActiveSupport loaded — ice_cube has no runtime dependency on it. Zones now resolve against ActiveSupport when present, then TZInfo, and otherwise warn and fall back to zone-less parsing. (The TZInfo result is flattened to a plain fixed-offset Time: TZInfo 2 returns a TimeWithOffset whose #zone is an abbreviation, which sends match_zone down its system-local branch and relocates every occurrence into the system zone. That path is DST-unaware after the start time and warns once to say so.)

TZID is now read from any parameter position. Only the first parameter was inspected, so DTSTART;VALUE=DATE-TIME;TZID=America/New_York:... silently lost its zone and fell back to the system zone.

Quoted TZID values are accepted, and the content line is split at the first colon that is not inside a quoted parameter — quoting exists precisely to permit a colon in the value (RFC 5545 §3.1 and §3.2.19). The TZID parameter name is also matched case-insensitively.

An unresolvable TZID warns instead of silently falling back to the system zone. Outlook emits Windows zone names such as TZID=Eastern Standard Time, which hit this constantly and produced silently wrong times.

Builder

Serializes the IANA identifier rather than the ActiveSupport zone label. A schedule built with Time.zone = "Eastern Time (US & Canada)" emitted TZID=Eastern Time (US & Canada). That round-trips within ice_cube, which is why the specs passed, but it is meaningless to every other iCalendar implementation — relevant to @wwahammy's question above about standards conformance and the Postgres use case. It now emits America/New_York. This is why the to_ical spec expectations changed.

Warns once per process rather than on every occurrence when coercing a zone-less Time to UTC. The suite alone emitted this 25 times.

Also folded in the unresolved review comments on this PR: the reversivible typo, the "Coercing into UTC" wording, moving the helper into TimeUtil, and the warning on the unknown-zone fallback.

Known gaps, not addressed here

Pre-existing issues found in the same audit and deliberately left out to keep this PR reviewable:

  • Floating values are interpreted in the system zone, not DTSTART's zone. EXDATE:20130102T090000 under a zoned DTSTART silently fails to exclude the occurrence, and a floating UNTIL drops the last one. Fixing this needs a two-pass parse to establish the schedule's zone before reading the other properties.
  • Property names are matched case-sensitively. RFC 5545 §3.1 says they are case-insensitive; dtstart:... is silently ignored and the schedule starts at Time.now.
  • DURATION is still a no-op (the existing # FIXME), and VALUE=DATE all-day events become midnight in the system zone.

Happy to take any of these on in a follow-up if that would help.

Suite: 1259 examples, 0 failures; standardrb clean.

🤖 Generated with Claude Code

@wwahammy

wwahammy commented Aug 26, 2025

Copy link
Copy Markdown

@sergioisidoro this is great work! I could really use this feature, do you need any help on getting this PR across the finish line?

@sergioisidoro

sergioisidoro commented Aug 26, 2025

Copy link
Copy Markdown
Author

@wwahammy I'm fairly pessimistic about seeing this merged in this project. This is a breaking change, and the last commit to main has been done last year. There have been other people struggling with this issue with some atempts at solving it, and they have not been merged.

At the moment I'm using my fork installing the gem through git, and I've been considering publishing my fork to rubygems for our own use, as we're starting to use recurrent events more and more, with iCal as serialization format with the API.

But if you find any way of helping this through the finnish line, I would appreciate it. Or if we would find enough people to help with the maintenace of a separate fork?

@wwahammy

Copy link
Copy Markdown

But if you find any way of helping this through the finnish line, I would appreciate it. Or if we would find enough people to help with the maintenace of a separate fork?

I don't think I have any more influence than you on the project. I'd be open to helping with the fork.

@wwahammy

wwahammy commented Aug 27, 2025

Copy link
Copy Markdown

Oh, one thing I had been meaning to ask: is the format you use for the timezones follow the iCal standard? I haven't looked into it so I wasn't sure but I wanted to know.

On particular use-case is that, if we can serialize to standard iCal, we could use a postgres extensions supporting iCal to do actual queries based upon the values.

@sergioisidoro

Copy link
Copy Markdown
Author

is the format you use for the timezones follow the iCal standard?

Yes, and no. The standard does not specify a specific time zone format

Note: The specification of a global time zone registry is not
addressed by this document and is left for future study.
However, implementers may find the Olson time zone database [TZ]
a useful reference. It is an informal, public-domain collection
of time zone information, which is currently being maintained by
volunteer Internet participants, and is used in several
operating systems. This database contains current and historical
time zone information for a wide variety of locations around the
globe; it provides a time zone identifier for every unique time
zone rule set in actual use since 1970, with historical data
going back to the introduction of standard time.

https://www.ietf.org/rfc/rfc2445.txt

However, it suggests the Olson tz database - ie the timezone database matinained by IANA. The current example of the timezone identifier can be seen here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

And as you can see there are same abbreviations for multiple timezones (the old implementation).
This implementation works with at least the javascript library implementations of the iCal standard.

From postgres docs I can see that the "full timezome" format is supported as input

Screenshot 2025-08-28 at 11 17 39

@pacso

pacso commented Aug 29, 2025

Copy link
Copy Markdown
Collaborator

Hello,

Thank you for your work on this! Am keen to keep this project alive. I will try to put some time aside this weekend to review this PR properly and see if there's a way we can get this merged.

@wwahammy

Copy link
Copy Markdown

Hello,

Thank you for your work on this! Am keen to keep this project alive. I will try to put some time aside this weekend to review this PR properly and see if there's a way we can get this merged.

Thank you for all of the work you do @pacso! Totally get that you're doing this on the side and that's a lot of work. If you're open to it, it might make sense to work together on developing a path forward that spreads the workload more so you're not responsible for so much. We're all benefiting so we should all pitch-in after all.

@sergioisidoro sergioisidoro left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I self reviewed the PR :) there were a few typos and a small nitpick. This work was done a bit in a hurry to fix our bugs with the lost time in serialization.

Let me know if there's anything else that needs attention on this PR, and I'll fixe everything in one go

Comment thread lib/ice_cube/builders/ical_builder.rb Outdated
Comment thread lib/ice_cube/builders/ical_builder.rb Outdated
Comment thread lib/ice_cube/parsers/ical_parser.rb Outdated
Comment thread lib/ice_cube/parsers/ical_parser.rb Outdated
@sergioisidoro

sergioisidoro commented Sep 8, 2026

Copy link
Copy Markdown
Author

Status?

@wwahammy are you still open to help maintaining a fork?

Follow-up fixes to the TZID support added earlier on this branch, found by
auditing the round-trip for issues of the same class.

Parser:

- Do not require ActiveSupport. The TZID path called String#in_time_zone,
  which raised NoMethodError for anyone parsing an iCal string containing a
  TZID without ActiveSupport loaded; ice_cube has no runtime dependency on
  it. Zones now resolve against ActiveSupport when present, then TZInfo, and
  otherwise warn and fall back to zone-less parsing.
- Read TZID from any parameter position. Only the first parameter was
  inspected, so DTSTART;VALUE=DATE-TIME;TZID=America/New_York silently lost
  its zone and fell back to the system zone.
- Accept double-quoted TZID values, and split the content line at the first
  colon that is not inside a quoted parameter, since quoting exists precisely
  to allow a colon in the value (RFC 5545 sections 3.1 and 3.2.19).
- Match the TZID parameter name case-insensitively.
- Warn instead of silently falling back to the system zone for an
  unresolvable TZID, which Outlook's Windows zone names hit routinely.

Builder:

- Serialize the IANA identifier rather than the ActiveSupport zone label.
  A schedule built with Time.zone = "Eastern Time (US & Canada)" emitted
  TZID=Eastern Time (US & Canada), which round-trips within ice_cube but is
  meaningless to every other iCalendar implementation.
- Warn once per process, not on every occurrence, when coercing a zone-less
  Time to UTC. The suite alone emitted this 25 times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tjCXM4KcVq2wjuj2QAZkJ
@wwahammy

wwahammy commented Sep 8, 2026

Copy link
Copy Markdown

Status?

@wwahammy are you still open to help maintaining a fork?

Hi @sergioisidoro, yes I'm still open to it!


@missing_time_zone_warned = true
warn "IceCube: Time object does not have timezone info. Coercing into UTC: #{caller(2..2).first}"
end

@wwahammy wwahammy Sep 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this warn change should be its own PR, it's not really related to preserving timezones in ical. EDIT: Oops, nevermind, I realized you added the warn in this PR originally. Again, 7:30 in the morning 😄


Additionally, it's 7:30 in the morning so I might be reading this wrong but are we sure this is actually going to warn once per process? Is there a situation where the class gets reloaded or there's some sort of race condition on this?

@sergioisidoro sergioisidoro Sep 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added this warning because it was one of the reasons I made this PR - it defaulted to falling back to UTC without anything surfaced to the dev. If I would have seen this in the initial test runs, it would have been clear that the behaviour was not expected. I think this also serves a bit as a mitigation for the breaking change.

Good question. This change was introduced by claude code in the lastest round of reviewing (which fixed the ActiveRecord dependency issue). I previosuly was issuing warning per instance, which could easily overwhelm the logs. First question is -- is it enough to warn once, to allow the dev to know of this behaviour? Or is issuing a warning per instance actually desired?

About the per process, I had to search this. Ruby classes are instances of a Class, so that instance variable missing_time_zone_warned will be set once per class when it is loaded. I assume that a reload will re-instantiate that Class instance 🤔 It solves the problem of spamming the logs, but the comment may be factually incorrect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants