diff --git a/lib/icalendar.ex b/lib/icalendar.ex index 3d0dc31..18c4736 100644 --- a/lib/icalendar.ex +++ b/lib/icalendar.ex @@ -5,7 +5,7 @@ defmodule ICalendar do defstruct events: [] defdelegate to_ics(events, options \\ []), to: ICalendar.Serialize - defdelegate from_ics(events), to: ICalendar.Deserialize + defdelegate from_ics(events, options \\ []), to: ICalendar.Deserialize @doc """ To create a Phoenix/Plug controller and view that output ics format: diff --git a/lib/icalendar/deserialize.ex b/lib/icalendar/deserialize.ex index 7619705..aa3fa60 100644 --- a/lib/icalendar/deserialize.ex +++ b/lib/icalendar/deserialize.ex @@ -1,40 +1,51 @@ defprotocol ICalendar.Deserialize do - def from_ics(ics) + def from_ics(ics, opts \\ []) end alias ICalendar.Deserialize defimpl ICalendar.Deserialize, for: BitString do alias ICalendar.Util.Deserialize + require Logger - def from_ics(ics) do + def from_ics(ics, opts \\ []) do ics |> String.trim() |> String.split("\n") |> Enum.map(&String.trim_trailing/1) - |> get_events() + |> get_events([], [], opts) end - defp get_events(calendar_data, event_collector \\ [], temp_collector \\ []) + defp get_events(calendar_data, event_collector, temp_collector, opts) - defp get_events([head | calendar_data], event_collector, temp_collector) do + defp get_events([head | calendar_data], event_collector, temp_collector, opts) do case head do "BEGIN:VEVENT" -> # start collecting event - get_events(calendar_data, event_collector, [head]) + get_events(calendar_data, event_collector, [head], opts) "END:VEVENT" -> # finish collecting event - event = Deserialize.build_event(temp_collector ++ [head]) - get_events(calendar_data, [event] ++ event_collector, []) + try do + event = Deserialize.build_event(temp_collector ++ [head]) + get_events(calendar_data, [event] ++ event_collector, [], opts) + rescue + e -> + if Keyword.get(opts, :ignore_errors) do + Logger.info("Error parsing: #{inspect(e)}") + get_events(calendar_data, event_collector, [], opts) + else + Kernel.reraise(e, __STACKTRACE__) + end + end event_property when temp_collector != [] -> - get_events(calendar_data, event_collector, temp_collector ++ [event_property]) + get_events(calendar_data, event_collector, temp_collector ++ [event_property], opts) _unimportant_stuff -> - get_events(calendar_data, event_collector, temp_collector) + get_events(calendar_data, event_collector, temp_collector, opts) end end - defp get_events([], event_collector, _temp_collector), do: event_collector + defp get_events([], event_collector, _temp_collector, _opts), do: event_collector end diff --git a/lib/icalendar/util/deserialize.ex b/lib/icalendar/util/deserialize.ex index a6d1443..a0bebc1 100644 --- a/lib/icalendar/util/deserialize.ex +++ b/lib/icalendar/util/deserialize.ex @@ -102,12 +102,19 @@ defmodule ICalendar.Util.Deserialize do end def parse_attr( - %Property{key: "EXDATE", value: exdate, params: params}, + %Property{key: "EXDATE", value: new_exdates, params: params}, acc ) do exdates = Map.get(acc, :exdates, []) - {:ok, timestamp} = to_date(exdate, params) - %{acc | exdates: [timestamp | exdates]} + + new_dates = + String.split(new_exdates, ",") + |> Enum.map(fn exdate -> + {:ok, timestamp} = to_date(exdate, params) + timestamp + end) + + %{acc | exdates: Enum.concat(new_dates, exdates)} end def parse_attr( @@ -238,8 +245,13 @@ defmodule ICalendar.Util.Deserialize do if Regex.match?(~r/\//, timezone) do timezone else - Timex.Timezone.Utils.to_olson(timezone) - end + try do + Timex.Timezone.Utils.to_olson(timezone) + rescue + # probably a custom timezone defined in this file + _e -> nil + end + end || "UTC" date_string = case String.last(date_string) do diff --git a/test/icalendar/deserialize_test.exs b/test/icalendar/deserialize_test.exs index b02ba6e..e49c536 100644 --- a/test/icalendar/deserialize_test.exs +++ b/test/icalendar/deserialize_test.exs @@ -49,6 +49,41 @@ defmodule ICalendar.DeserializeTest do assert event.dtend.time_zone == "America/Chicago" end + test "with custom Timezone won't crash" do + ics = """ + BEGIN:VTIMEZONE + TZID:Eastern Standard Time 1 + BEGIN:STANDARD + DTSTART:16010101T000000 + TZOFFSETFROM:-0500 + TZOFFSETTO:-0500 + END:STANDARD + END:VTIMEZONE + BEGIN:VEVENT + DTEND;TZID=Eastern Standard Time 1:22221224T084500 + DTSTART;TZID=Eastern Standard Time 1:22221224T083000 + END:VEVENT + """ + + [event] = ICalendar.from_ics(ics) + assert event.dtstart.time_zone == "Etc/UTC" + assert event.dtend.time_zone == "Etc/UTC" + end + + test "with rrule exceptions" do + ics = """ + BEGIN:VEVENT + RRULE:FREQ=WEEKLY + EXDATE;TZID=Eastern Standard Time:20201126T091500,20201127T091500 + DTSTART;TZID=Eastern Standard Time:20201023T091500 + DTEND;TZID=Eastern Standard Time:20201023T093000 + END:VEVENT + """ + + [event] = ICalendar.from_ics(ics) + assert length(event.exdates) == 2 + end + test "with CR+LF line endings" do ics = """ BEGIN:VEVENT