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