diff --git a/lib/ical/deserialize.ex b/lib/ical/deserialize.ex index 8909010..a5bb995 100644 --- a/lib/ical/deserialize.ex +++ b/lib/ical/deserialize.ex @@ -442,13 +442,6 @@ defmodule ICal.Deserialize do @spec to_date(String.t() | nil, map, ICal.t()) :: Date.t() | DateTime.t() | nil def to_date(nil, _params, _calendar), do: nil - def to_date(date_string, %{"TZID" => timezone}, %ICal{default_timezone: default_timezone}) do - # Microsoft Outlook calendar .ICS files report times in Greenwich Standard Time (UTC +0) - # so just convert this to UTC - timezone = to_timezone(timezone, default_timezone) - to_date_in_timezone(date_string, timezone) - end - def to_date(date_string, %{"VALUE" => "DATE"}, _calendar) do # of the form {YYYY}{MM}{DD} with <> <- date_string, @@ -462,6 +455,14 @@ defmodule ICal.Deserialize do end end + def to_date(date_string, %{"TZID" => timezone}, %ICal{default_timezone: default_timezone}) do + # Microsoft Outlook calendar .ICS files report times in Greenwich Standard Time (UTC +0) + # so just convert this to UTC + timezone = to_timezone(timezone, default_timezone) + + to_date_in_timezone(date_string, timezone) + end + def to_date(date_string, _params, %ICal{default_timezone: default_timezone}) do to_date_in_timezone(date_string, default_timezone) end diff --git a/test/data/date_only_event.ics b/test/data/date_only_event.ics new file mode 100644 index 0000000..75ce5d0 --- /dev/null +++ b/test/data/date_only_event.ics @@ -0,0 +1,36 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:ExampleProd +VERSION:2.0 +X-WR-CALNAME:ExampleName +X-WR-TIMEZONE:America/Edmonton +BEGIN:VTIMEZONE +TZID:America/Edmonton +X-LIC-LOCATION:America/Edmonton +BEGIN:STANDARD +DTSTART:20241103T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +TZNAME:MST +TZOFFSETFROM:-0600 +TZOFFSETTO:-0700 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20250309T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +TZNAME:MDT +TZOFFSETFROM:-0700 +TZOFFSETTO:-0600 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +CREATED;TZID=America/Edmonton:20260406T000000 +DTEND;TZID=America/Edmonton;VALUE=DATE:20260530 +DTSTAMP;TZID=America/Edmonton:20260406T000000 +DTSTART;TZID=America/Edmonton;VALUE=DATE:20260529 +SEQUENCE:2026052815 +SUMMARY:ExampleSummary +UID:ExampleID +X-FUNAMBOL-ALLDAY:1 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +END:VEVENT +END:VCALENDAR diff --git a/test/ical/deserialize_test.exs b/test/ical/deserialize_test.exs index d53ed09..5b6c9ca 100644 --- a/test/ical/deserialize_test.exs +++ b/test/ical/deserialize_test.exs @@ -234,6 +234,20 @@ defmodule ICal.DeserializeTest do assert ICal.from_file("/does/not/exist.ics") == {:error, :enoent} end + test "Deserializing DATE fields with TZID params works correctly " do + # According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.19 + # this is not a RFC-conformant calendar, but ICal should still be able to parse it. + ics = Helper.test_data("date_only_event") + calendar = ICal.from_ics(ics) + + assert Enum.count(calendar.events) == 1 + + [event] = calendar.events + + assert event.dtstart == ~D[2026-05-29] + assert event.dtend == ~D[2026-05-30] + end + test "Bad separators do not disturb parsing" do ics = Helper.test_data("broken_uid") %ICal{events: [event]} = ICal.from_ics(ics) diff --git a/test/ical_test.exs b/test/ical_test.exs index df5448b..8c0bc67 100644 --- a/test/ical_test.exs +++ b/test/ical_test.exs @@ -197,7 +197,7 @@ defmodule ICalTest do |> assert_fully_contains(expected) end - test "ICalender.to_ics/1 with exdates" do + test "ICal.to_ics/1 with exdates" do events = [ %ICal.Event{ exdates: [ @@ -216,7 +216,7 @@ defmodule ICalTest do assert ics =~ "EXDATE;TZID=America/Toronto:20200917T143000" end - test "ICalender.to_ics/1 with duration" do + test "ICal.to_ics/1 with duration" do events = [ %ICal.Event{ duration: %ICal.Duration{ @@ -236,7 +236,7 @@ defmodule ICalTest do assert ics =~ "DURATION:P15DT5H20S" end - test "ICalender.to_ics/1 with RECURRENCE-ID in UTC" do + test "ICal.to_ics/1 with RECURRENCE-ID in UTC" do events = [ %ICal.Event{ recurrence_id: ~U[2020-09-17 14:30:00Z], @@ -252,7 +252,7 @@ defmodule ICalTest do assert ics =~ "RECURRENCE-ID:20200917T143000Z" end - test "ICalender.to_ics/1 with RECURRENCE-ID with timezone" do + test "ICal.to_ics/1 with RECURRENCE-ID with timezone" do recurrence_id = DateTime.shift_zone!(~U[2020-09-17 18:30:00Z], "America/Toronto") events = [ @@ -270,7 +270,7 @@ defmodule ICalTest do assert ics =~ "RECURRENCE-ID;TZID=America/Toronto:20200917T143000" end - test "ICalender.to_ics/1 -> ICal.from_ics/1 and back again" do + test "ICal.to_ics/1 -> ICal.from_ics/1 and back again" do events = [ %ICal.Event{ summary: "Film with Amy and Adam", @@ -292,7 +292,7 @@ defmodule ICalTest do assert events |> List.first() == new_event end - test "ICalender.to_ics/1 -> ICal.from_ics/1 and back again, with newlines" do + test "ICal.to_ics/1 -> ICal.from_ics/1 and back again, with newlines" do events = [ %ICal.Event{ summary: "Film with Amy and Adam", @@ -318,7 +318,7 @@ defmodule ICalTest do Regex.scan(~r/#{check_for}/, ics) |> Enum.count() end - test "ICalender.to_ics/1 supports bare components and lists of components" do + test "ICal.to_ics/1 supports bare components and lists of components" do assert %ICal{events: [%ICal.Event{}]} |> ICal.to_ics() |> to_string()