feat: add --timezone CLI option for configurable output timezone

This commit is contained in:
2026-05-18 14:51:05 +02:00
parent 37ecfd6130
commit 527669227b
3 changed files with 26 additions and 12 deletions

View File

@@ -5,12 +5,16 @@ let files =
let doc = "Files to process" in let doc = "Files to process" in
Arg.(non_empty & pos_all string [] & info [] ~docv:"FILE" ~doc) Arg.(non_empty & pos_all string [] & info [] ~docv:"FILE" ~doc)
let timezone =
let doc = "Target timezone for output (e.g. Europe/Rome). Defaults to local timezone." in
Arg.(value & opt (some string) None & info [ "timezone"; "z" ] ~docv:"TZ" ~doc)
let main_command f = let main_command f =
let doc = "Convert iCalendar files to remind format" in let doc = "Convert iCalendar files to remind format" in
let man = [] in let man = [] in
Cmd.make (Cmd.info "ical2rem" ~version:"%%VERSION%%" ~doc ~man) Cmd.make (Cmd.info "ical2rem" ~version:"%%VERSION%%" ~doc ~man)
@@ @@
let+ files = files in let+ files = files and+ tz = timezone in
f files f tz files
let main f = Cmd.eval @@ main_command f let main f = Cmd.eval @@ main_command f

View File

@@ -14,7 +14,8 @@ let read_file filename =
close_in ic; close_in ic;
Bytes.unsafe_to_string s Bytes.unsafe_to_string s
let ical2rem ical_files = let ical2rem tz_opt ical_files =
Utils.init_target_tz tz_opt;
let good_rems = let good_rems =
ListLabels.fold_left ~init:[] ical_files ~f:(fun good_rems_acc filename -> ListLabels.fold_left ~init:[] ical_files ~f:(fun good_rems_acc filename ->
try try

View File

@@ -1,5 +1,14 @@
open Icalendar open Icalendar
(** Target timezone for all timestamp conversions. Defaults to local timezone; overridden by --timezone CLI option
before any processing begins. *)
let target_tz : Timedesc.Time_zone.t ref = ref Timedesc.Time_zone.utc
let init_target_tz (tz_opt : string option) : unit =
match tz_opt with
| None -> target_tz := Timedesc.Time_zone.local_exn ()
| Some name -> target_tz := Timedesc.Time_zone.make_exn name
type months = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec type months = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec
let timedesc_wd_to_ical (wd : Timedesc.weekday) : Icalendar.weekday = let timedesc_wd_to_ical (wd : Timedesc.weekday) : Icalendar.weekday =
@@ -89,28 +98,28 @@ let string_of_span (sp : Timedesc.Span.t) : string =
spf "%02d:%02d" hours minutes spf "%02d:%02d" hours minutes
let timedesc_of_timestamp (ts : timestamp) : Timedesc.t = let timedesc_of_timestamp (ts : timestamp) : Timedesc.t =
let local_tz = Timedesc.Time_zone.local_exn () in
match ts with match ts with
| `Local t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:local_tz | `Local t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:!target_tz
(* this case is not present in my current dataset… *) (* 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 | `Utc t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:!target_tz
| `With_tzid (ts, (_b, tz_name)) -> | `With_tzid (ts, (_b, tz_name)) ->
(* Qui il timestamp è SCRITTO come se fosse UTC (+00:00) ma in realtà va interpretato con (* The timestamp is stored as if it were UTC but must be interpreted in tz_name.
il fuso orario indicato da tz_name. *) We reconstruct the wall-clock time in tz_name, then convert to target_tz. *)
let tz = Timedesc.Time_zone.make_exn tz_name in let tz = Timedesc.Time_zone.make_exn tz_name in
let wrong_ts = Timedesc.Utils.timestamp_of_ptime ts in let wrong_ts = Timedesc.Utils.timestamp_of_ptime ts in
let date = Timedesc.date (Timedesc.of_timestamp_exn ~tz_of_date_time:Timedesc.Time_zone.utc wrong_ts) in let date = Timedesc.date (Timedesc.of_timestamp_exn ~tz_of_date_time:Timedesc.Time_zone.utc wrong_ts) in
let year, month, day = (Timedesc.Date.year date, Timedesc.Date.month date, Timedesc.Date.day date) in let year, month, day = (Timedesc.Date.year date, Timedesc.Date.month date, Timedesc.Date.day date) in
let time = Timedesc.time_view (Timedesc.of_timestamp_exn ~tz_of_date_time:Timedesc.Time_zone.utc wrong_ts) in let time = Timedesc.time_view (Timedesc.of_timestamp_exn ~tz_of_date_time:Timedesc.Time_zone.utc wrong_ts) in
let hour, minute, second = (time.Timedesc.Time.hour, time.Timedesc.Time.minute, time.Timedesc.Time.second) in let hour, minute, second = (time.Timedesc.Time.hour, time.Timedesc.Time.minute, time.Timedesc.Time.second) in
Timedesc.make_exn ~year ~month ~day ~hour ~minute ~second ~tz () let t_in_named_tz = Timedesc.make_exn ~year ~month ~day ~hour ~minute ~second ~tz () in
(* Convert from tz_name to target_tz *)
Timedesc.of_timestamp_exn ~tz_of_date_time:!target_tz (Timedesc.to_timestamp_single t_in_named_tz)
let timedesc_of_utc_or_timestamp_local (ts : utc_or_timestamp_local) : Timedesc.t = let timedesc_of_utc_or_timestamp_local (ts : utc_or_timestamp_local) : Timedesc.t =
let local_tz = Timedesc.Time_zone.local_exn () in
match ts with match ts with
| `Local t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:local_tz | `Local t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:!target_tz
(* this case is not present in my current dataset… *) (* 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 | `Utc t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:!target_tz
(** Convert a UTC-or-local timestamp to a Timedesc.t in the given timezone. Use this (instead of (** 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 [timedesc_of_utc_or_timestamp_local]) when the event has a known TZID, so that UNTIL comparisons are independent of