From 1372d330ffb1283fdcae3d16d579d334ca76771c Mon Sep 17 00:00:00 2001 From: Paolo Donadeo Date: Mon, 18 May 2026 00:17:04 +0200 Subject: [PATCH] fix: use event TZID for UNTIL date conversion Add a `tz` field to `rem` to carry the event's DTSTART timezone, and introduce `timedesc_of_utc_or_timestamp_tz` to convert UNTIL timestamps in that timezone instead of the process locale. This makes UNTIL comparisons locale-independent for events with a known TZID. --- bin/eventPredicates.ml | 9 ++++++++- bin/remind.ml | 11 ++++++++--- bin/utils.ml | 8 ++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bin/eventPredicates.ml b/bin/eventPredicates.ml index cbd1bcc..bd89832 100644 --- a/bin/eventPredicates.ml +++ b/bin/eventPredicates.ml @@ -202,7 +202,14 @@ let collect_start_end_duration rem ev : (Remind.rem, error) result = end) | `Datetime datetime -> begin let start_td = Utils.timedesc_of_timestamp datetime in - let rem = { rem with Remind.date = Timedesc.date start_td; Remind.time = Some (Timedesc.time start_td) } in + let rem = + { + rem with + Remind.date = Timedesc.date start_td; + Remind.time = Some (Timedesc.time start_td); + Remind.tz = Some (Timedesc.tz start_td); + } + in match ev.dtend_or_duration with | None -> Ok rem diff --git a/bin/remind.ml b/bin/remind.ml index 0f988e0..00b8811 100644 --- a/bin/remind.ml +++ b/bin/remind.ml @@ -45,6 +45,7 @@ type rem = { *) exdate : Icalendar.date_or_datetime list; (** List of excluded dates for recurring events *) overrides : rem list; (** Single-event REMs generated from non-cancelled RECURRENCE-ID overrides *) + tz : Timedesc.Time_zone.t option; (** Timezone of the event's DTSTART, used for UNTIL conversion *) } (** A complete REM command *) @@ -64,6 +65,7 @@ let empty = recurring = []; exdate = []; overrides = []; + tz = None; } (* ── buffer primitives ────────────────────────────────────────── *) @@ -97,7 +99,8 @@ let add_until b rem (w : simple_weekly) = match w.count_or_until with | None -> () | Some (`Until d) -> - let ts = timedesc_of_utc_or_timestamp_local d in + let tz = Option.value ~default:(Timedesc.Time_zone.local_exn ()) rem.tz in + let ts = timedesc_of_utc_or_timestamp_tz tz d in let date = until_date_adjusted ts rem.time in Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 date)) | Some (`Count count) -> @@ -116,7 +119,8 @@ let add_until_daily b rem (d : simple_daily) = match d.count_or_until with | None -> () | Some (`Until dt) -> - let ts = timedesc_of_utc_or_timestamp_local dt in + let tz = Option.value ~default:(Timedesc.Time_zone.local_exn ()) rem.tz in + let ts = timedesc_of_utc_or_timestamp_tz tz dt in let date = until_date_adjusted ts rem.time in Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 date)) | Some (`Count count) -> @@ -128,7 +132,8 @@ let add_until_monthly b rem (m : simple_monthly) = match m.count_or_until with | None -> () | Some (`Until dt) -> - let ts = timedesc_of_utc_or_timestamp_local dt in + let tz = Option.value ~default:(Timedesc.Time_zone.local_exn ()) rem.tz in + let ts = timedesc_of_utc_or_timestamp_tz tz dt in let date = until_date_adjusted ts rem.time in Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 date)) | Some (`Count count) -> diff --git a/bin/utils.ml b/bin/utils.ml index e1cd9d7..27ca2a2 100644 --- a/bin/utils.ml +++ b/bin/utils.ml @@ -112,6 +112,14 @@ let timedesc_of_utc_or_timestamp_local (ts : utc_or_timestamp_local) : Timedesc. (* this case is not present in my current dataset… *) | `Utc t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:local_tz +(** Convert a UTC-or-local timestamp to a Timedesc.t in the given timezone. Use this (instead of + [timedesc_of_utc_or_timestamp_local]) when the event has a known TZID, so that UNTIL comparisons are independent of + the process locale. *) +let timedesc_of_utc_or_timestamp_tz (tz : Timedesc.Time_zone.t) (ts : utc_or_timestamp_local) : Timedesc.t = + match ts with + | `Local t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:tz + | `Utc t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:tz + let get_exdates ev = let event_props = ev.props in let dates_or_datetimes =