feat: initial implementation of iCalendar to Remind converter
- Add project scaffolding (dune, dune-project, opam, .ocamlformat) - Implement basic parsing and handling of iCalendar events - Add event predicates for common event types (all-day, timed, recurrence, exceptions) - Add transformation logic to map iCalendar events to Remind format (stub implementation) - Provide utilities for extracting event details and converting dates/times - Set up executable entrypoint and command-line interface using Cmdliner - Include Remind event type definitions and helpers
This commit is contained in:
19
.ocamlformat
Normal file
19
.ocamlformat
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version = 0.28.1
|
||||||
|
profile = conventional
|
||||||
|
|
||||||
|
break-cases = fit-or-vertical
|
||||||
|
break-infix = fit-or-vertical
|
||||||
|
break-separators = after
|
||||||
|
cases-exp-indent = 2
|
||||||
|
exp-grouping = preserve
|
||||||
|
if-then-else = keyword-first
|
||||||
|
leading-nested-match-parens = false
|
||||||
|
let-and = sparse
|
||||||
|
margin = 120
|
||||||
|
space-around-arrays = false
|
||||||
|
space-around-lists = false
|
||||||
|
space-around-records = false
|
||||||
|
space-around-records = true
|
||||||
|
space-around-variants = false
|
||||||
|
type-decl = sparse
|
||||||
|
wrap-fun-args = false
|
||||||
17
bin/commandLine.ml
Normal file
17
bin/commandLine.ml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
open Cmdliner
|
||||||
|
open Cmdliner.Term.Syntax
|
||||||
|
|
||||||
|
let ical_file =
|
||||||
|
let doc = "TODO" in
|
||||||
|
let docv = "ICAL" in
|
||||||
|
Arg.(required & pos ~rev:true 0 (some string) None & info [] ~docv ~doc)
|
||||||
|
|
||||||
|
let main_command f =
|
||||||
|
let doc = "Convert iCalendar files to remind format" in
|
||||||
|
let man = [] in
|
||||||
|
Cmd.make (Cmd.info "ical2rem" ~version:"%%VERSION%%" ~doc ~man)
|
||||||
|
@@
|
||||||
|
let+ ical_file = ical_file in
|
||||||
|
f ical_file
|
||||||
|
|
||||||
|
let main f = Cmd.eval @@ main_command f
|
||||||
13
bin/dune
Normal file
13
bin/dune
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
(executable
|
||||||
|
(public_name remind_sync)
|
||||||
|
(name main)
|
||||||
|
(modules main commandLine remind eventTransformer eventPredicates utils)
|
||||||
|
(preprocess
|
||||||
|
(pps ppx_deriving.show))
|
||||||
|
(libraries
|
||||||
|
;remind_sync
|
||||||
|
cmdliner
|
||||||
|
icalendar
|
||||||
|
timedesc-tzdb.full
|
||||||
|
timedesc-tzlocal.unix
|
||||||
|
timedesc))
|
||||||
2301
bin/eventPredicates.ml
Normal file
2301
bin/eventPredicates.ml
Normal file
File diff suppressed because it is too large
Load Diff
15
bin/eventTransformer.ml
Normal file
15
bin/eventTransformer.ml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
let default_implementation = Remind.Omit (Ptime.epoch |> Ptime.to_date)
|
||||||
|
|
||||||
|
let remind_of_event (ev : Icalendar.event) : Remind.event =
|
||||||
|
let found =
|
||||||
|
ListLabels.fold_left ~init:[] EventPredicates.all_predicates ~f:(fun acc (pred, desc) ->
|
||||||
|
if pred ev then desc :: acc else acc)
|
||||||
|
|> List.rev
|
||||||
|
in
|
||||||
|
if List.length found > 0
|
||||||
|
then begin
|
||||||
|
Printf.printf " ⇒ matches these predicates:\n";
|
||||||
|
List.iter (fun d -> Printf.printf " - %s\n" (EventPredicates.show_event_description d)) found;
|
||||||
|
Printf.printf "\n"
|
||||||
|
end;
|
||||||
|
default_implementation
|
||||||
34
bin/main.ml
Normal file
34
bin/main.ml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
let ical2rem ical_file =
|
||||||
|
let ic = open_in ical_file in
|
||||||
|
let n = in_channel_length ic in
|
||||||
|
let s = Bytes.create n in
|
||||||
|
really_input ic s 0 n;
|
||||||
|
close_in ic;
|
||||||
|
let cal_or_error = Icalendar.parse (Bytes.unsafe_to_string s) in
|
||||||
|
match cal_or_error with
|
||||||
|
| Error e -> prerr_endline ("Error parsing iCalendar file: " ^ e)
|
||||||
|
| Ok (_, components) -> begin
|
||||||
|
let events = ref 0 in
|
||||||
|
List.iter
|
||||||
|
(fun comp ->
|
||||||
|
match comp with
|
||||||
|
| `Event event ->
|
||||||
|
events := !events + 1;
|
||||||
|
let uid = Utils.get_uid event in
|
||||||
|
Printf.printf " ⇒ UID: %s\n" uid;
|
||||||
|
Printf.printf "%s\n\n\n" (Icalendar.show_component comp)
|
||||||
|
| _ -> () (* Ignore non-event components *))
|
||||||
|
components;
|
||||||
|
Printf.printf "\nEvents: %d\n" !events;
|
||||||
|
let events =
|
||||||
|
List.filter_map
|
||||||
|
(function
|
||||||
|
| `Event ev -> Some ev
|
||||||
|
| _ -> None)
|
||||||
|
components
|
||||||
|
in
|
||||||
|
let _reminders = List.map EventTransformer.remind_of_event events in
|
||||||
|
()
|
||||||
|
end
|
||||||
|
|
||||||
|
let () = if !Sys.interactive then () else exit (CommandLine.main ical2rem)
|
||||||
119
bin/remind.ml
Normal file
119
bin/remind.ml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
(* Alias esplicito per chiarezza *)
|
||||||
|
type date = Ptime.date
|
||||||
|
|
||||||
|
let pp_date fmt (y, m, d) = Format.fprintf fmt "%04d-%02d-%02d" y m d
|
||||||
|
|
||||||
|
let show_date (d : date) : string =
|
||||||
|
let y, m, day = d in
|
||||||
|
Printf.sprintf "%04d-%02d-%02d" y m day
|
||||||
|
|
||||||
|
type tod = Ptime.Span.t (* secondi da mezzanotte locale *)
|
||||||
|
|
||||||
|
let pp_tod = Ptime.Span.pp
|
||||||
|
let span_min (m : int) : tod = Ptime.Span.of_int_s (m * 60)
|
||||||
|
let span_hm ~(h : int) ~(m : int) : tod = Ptime.Span.of_int_s ((h * 3600) + (m * 60))
|
||||||
|
|
||||||
|
let hm_of_tod (t : tod) : int * int =
|
||||||
|
match Ptime.Span.to_int_s t with
|
||||||
|
| None -> invalid_arg "tod out of range"
|
||||||
|
| Some s -> (s / 3600, s mod 3600 / 60)
|
||||||
|
|
||||||
|
type weekday =
|
||||||
|
| Mon
|
||||||
|
| Tue
|
||||||
|
| Wed
|
||||||
|
| Thu
|
||||||
|
| Fri
|
||||||
|
| Sat
|
||||||
|
| Sun
|
||||||
|
|
||||||
|
type time_spec =
|
||||||
|
| All_day
|
||||||
|
| Timed of {
|
||||||
|
at : tod;
|
||||||
|
duration : Ptime.Span.t option;
|
||||||
|
}
|
||||||
|
|
||||||
|
type range = {
|
||||||
|
from_ : date;
|
||||||
|
until : date option;
|
||||||
|
}
|
||||||
|
(* UNTIL inclusivo *)
|
||||||
|
|
||||||
|
type recurrence =
|
||||||
|
| No_recur
|
||||||
|
| Daily of {
|
||||||
|
interval : int;
|
||||||
|
span : range;
|
||||||
|
}
|
||||||
|
| Weekly of {
|
||||||
|
interval : int;
|
||||||
|
days : weekday list;
|
||||||
|
span : range;
|
||||||
|
}
|
||||||
|
| Monthly_dom of {
|
||||||
|
interval : int;
|
||||||
|
day : int;
|
||||||
|
span : range;
|
||||||
|
}
|
||||||
|
| Monthly_nth_weekday of {
|
||||||
|
interval : int;
|
||||||
|
n : int;
|
||||||
|
wday : weekday;
|
||||||
|
span : range;
|
||||||
|
}
|
||||||
|
| Yearly of {
|
||||||
|
month : int;
|
||||||
|
day : int;
|
||||||
|
}
|
||||||
|
|
||||||
|
type alarm = { warn_before : Ptime.Span.t }
|
||||||
|
|
||||||
|
type meta = {
|
||||||
|
location : string option;
|
||||||
|
url : string option;
|
||||||
|
categories : string list;
|
||||||
|
attendees : string list;
|
||||||
|
}
|
||||||
|
|
||||||
|
type instance_override = {
|
||||||
|
on : date;
|
||||||
|
cancel : bool;
|
||||||
|
summary : string option;
|
||||||
|
time_spec : time_spec option;
|
||||||
|
notes : string option;
|
||||||
|
alarm : alarm option;
|
||||||
|
meta : meta option;
|
||||||
|
}
|
||||||
|
|
||||||
|
type series = {
|
||||||
|
summary : string;
|
||||||
|
start : date; (* DTSTART locale *)
|
||||||
|
time_spec : time_spec;
|
||||||
|
recur : recurrence;
|
||||||
|
exdates : date list;
|
||||||
|
overrides : instance_override list;
|
||||||
|
alarm : alarm option;
|
||||||
|
notes : string option;
|
||||||
|
meta : meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
type info_header = string [@@deriving show]
|
||||||
|
type info_value = string [@@deriving show]
|
||||||
|
|
||||||
|
type event =
|
||||||
|
| Rem of {
|
||||||
|
date_expr : string;
|
||||||
|
at : tod option;
|
||||||
|
duration : Ptime.Span.t option;
|
||||||
|
warn : Ptime.Span.t option;
|
||||||
|
msg : string;
|
||||||
|
exdates : date list;
|
||||||
|
infos : (info_header * info_value) list;
|
||||||
|
}
|
||||||
|
| Omit of date
|
||||||
|
[@@deriving show]
|
||||||
|
|
||||||
|
let string_of_date (d : date) : string =
|
||||||
|
let y, m, day = d in
|
||||||
|
Printf.sprintf "%04d-%02d-%02d" y m day
|
||||||
70
bin/utils.ml
Normal file
70
bin/utils.ml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
open Icalendar
|
||||||
|
|
||||||
|
let get_uid ev =
|
||||||
|
let _, uid = ev.uid in
|
||||||
|
uid
|
||||||
|
|
||||||
|
let timedesc_of_date_or_datetime (t : date_or_datetime) : Timedesc.t =
|
||||||
|
match t with
|
||||||
|
| `Datetime (`Local _ptime_ts) ->
|
||||||
|
(* TODO: this case is not present in my current dataset… *)
|
||||||
|
failwith "Unhandled case: `Local datetime"
|
||||||
|
| `Datetime (`Utc ts) ->
|
||||||
|
Timedesc.Utils.timestamp_of_ptime ts |> Timedesc.of_timestamp_exn ~tz_of_date_time:(Timedesc.Time_zone.local_exn ())
|
||||||
|
| `Datetime (`With_tzid (ts, (_b, tz_name))) ->
|
||||||
|
(* Qui il timestamp è SCRITTO come se fosse UTC (+00:00) ma in realtà va interpretato con
|
||||||
|
il fuso orario indicato da tz_name. *)
|
||||||
|
let tz = Timedesc.Time_zone.make_exn tz_name 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 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 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 ()
|
||||||
|
| `Date (year, month, day) ->
|
||||||
|
Timedesc.make_exn ~year ~month ~day ~hour:0 ~minute:0 ~second:0 ~tz:(Timedesc.Time_zone.local_exn ()) ()
|
||||||
|
|
||||||
|
let get_start ev =
|
||||||
|
let _, start = ev.dtstart in
|
||||||
|
timedesc_of_date_or_datetime start
|
||||||
|
|
||||||
|
let get_exdates ev =
|
||||||
|
let event_props = ev.props in
|
||||||
|
let dates_or_datetimes =
|
||||||
|
List.filter_map
|
||||||
|
(fun prop ->
|
||||||
|
match prop with
|
||||||
|
| `Exdate (_, dates) -> Some dates
|
||||||
|
| _ -> None)
|
||||||
|
event_props
|
||||||
|
in
|
||||||
|
ListLabels.fold_left ~init:[] dates_or_datetimes ~f:(fun acc dates ->
|
||||||
|
let added =
|
||||||
|
match dates with
|
||||||
|
| `Datetimes ts_list -> List.map (fun ts -> `Datetime ts) ts_list
|
||||||
|
| `Dates date_list -> List.map (fun date -> `Date date) date_list
|
||||||
|
in
|
||||||
|
added @ acc)
|
||||||
|
|> List.map timedesc_of_date_or_datetime
|
||||||
|
|
||||||
|
let get_rdates ev =
|
||||||
|
let event_props = ev.props in
|
||||||
|
let dates_or_datetimes =
|
||||||
|
List.filter_map
|
||||||
|
(fun prop ->
|
||||||
|
match prop with
|
||||||
|
| `Rdate (_, dates) -> Some dates
|
||||||
|
| _ -> None)
|
||||||
|
event_props
|
||||||
|
in
|
||||||
|
ListLabels.fold_left ~init:[] dates_or_datetimes ~f:(fun acc dates ->
|
||||||
|
let added =
|
||||||
|
match dates with
|
||||||
|
| `Datetimes ts_list -> List.map (fun ts -> `Datetime ts) ts_list
|
||||||
|
| `Dates date_list -> List.map (fun date -> `Date date) date_list
|
||||||
|
| `Periods _ ->
|
||||||
|
(* TODO: Ignored for now, does not appear in my current dataset *)
|
||||||
|
failwith "Unhandled case: `Periods in RDATE"
|
||||||
|
in
|
||||||
|
added @ acc)
|
||||||
|
|> List.map timedesc_of_date_or_datetime
|
||||||
24
dune-project
Normal file
24
dune-project
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(lang dune 3.20)
|
||||||
|
|
||||||
|
(name remind_sync)
|
||||||
|
|
||||||
|
(generate_opam_files true)
|
||||||
|
|
||||||
|
(source
|
||||||
|
(uri https://git.donadeo.net/pdonadeo/remind-sync))
|
||||||
|
|
||||||
|
(authors "Paolo Donadeo <paolo@donadeo.net>")
|
||||||
|
|
||||||
|
(maintainers "Maintainer Name <maintainer@example.com>")
|
||||||
|
|
||||||
|
(license MIT)
|
||||||
|
|
||||||
|
(documentation https://git.donadeo.net/pdonadeo/remind-sync)
|
||||||
|
|
||||||
|
(package
|
||||||
|
(name remind_sync)
|
||||||
|
(synopsis "A short synopsis")
|
||||||
|
(description "A longer description")
|
||||||
|
(depends ocaml)
|
||||||
|
(tags
|
||||||
|
("add topics" "to describe" your project)))
|
||||||
30
remind_sync.opam
Normal file
30
remind_sync.opam
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file is generated by dune, edit dune-project instead
|
||||||
|
opam-version: "2.0"
|
||||||
|
synopsis: "A short synopsis"
|
||||||
|
description: "A longer description"
|
||||||
|
maintainer: ["Maintainer Name <maintainer@example.com>"]
|
||||||
|
authors: ["Paolo Donadeo <paolo@donadeo.net>"]
|
||||||
|
license: "MIT"
|
||||||
|
tags: ["add topics" "to describe" "your" "project"]
|
||||||
|
doc: "https://git.donadeo.net/pdonadeo/remind-sync"
|
||||||
|
depends: [
|
||||||
|
"dune" {>= "3.20"}
|
||||||
|
"ocaml"
|
||||||
|
"odoc" {with-doc}
|
||||||
|
]
|
||||||
|
build: [
|
||||||
|
["dune" "subst"] {dev}
|
||||||
|
[
|
||||||
|
"dune"
|
||||||
|
"build"
|
||||||
|
"-p"
|
||||||
|
name
|
||||||
|
"-j"
|
||||||
|
jobs
|
||||||
|
"@install"
|
||||||
|
"@runtest" {with-test}
|
||||||
|
"@doc" {with-doc}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
dev-repo: "https://git.donadeo.net/pdonadeo/remind-sync"
|
||||||
|
x-maintenance-intent: ["(latest)"]
|
||||||
Reference in New Issue
Block a user