Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions lib/ical/deserialize.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<y::binary-size(4), m::binary-size(2), d::binary-size(2)>> <- date_string,
Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions test/data/date_only_event.ics
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions test/ical/deserialize_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions test/ical_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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{
Expand All @@ -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],
Expand All @@ -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 = [
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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()
Expand Down
Loading