From 8a24bddbd3647bf990b9d3d76b495bfcebb58eec Mon Sep 17 00:00:00 2001 From: Thales Oliveira Date: Sun, 21 Oct 2018 18:19:07 -0300 Subject: [PATCH 1/2] Build dates without location info Due to Go's way of handling dates location, it is necessary to not use any location info when the lib builds the dates, letting the users of the lib handle the location themselves. --- event.go | 2 + parse.go | 109 +++++++++++++++++++++++++++----------------------- parse_test.go | 8 ++-- 3 files changed, 66 insertions(+), 53 deletions(-) diff --git a/event.go b/event.go index 9a47400..2b680c9 100644 --- a/event.go +++ b/event.go @@ -25,6 +25,8 @@ type Event struct { Organizer Attendee WholeDayEvent bool ExDates []time.Time + StartTimezone *time.Location + EndTimezone *time.Location } type byDate []Event diff --git a/parse.go b/parse.go index 7698cb6..092a365 100644 --- a/parse.go +++ b/parse.go @@ -167,43 +167,43 @@ var ( "Lord Howe Standard Time": "Australia/Lord_Howe", "W. Australia Standard Time": "Australia/Perth", "AUS Eastern Standard Time": "Australia/Sydney", - "UTC": "Etc/GMT", - "UTC-11": "Etc/GMT+11", - "Dateline Standard Time": "Etc/GMT+12", - "UTC-02": "Etc/GMT+2", - "UTC-08": "Etc/GMT+8", - "UTC-09": "Etc/GMT+9", - "UTC+12": "Etc/GMT-12", - "UTC+13": "Etc/GMT-13", - "Astrakhan Standard Time": "Europe/Astrakhan", - "W. Europe Standard Time": "Europe/Berlin", - "GTB Standard Time": "Europe/Bucharest", - "Central Europe Standard Time": "Europe/Budapest", - "E. Europe Standard Time": "Europe/Chisinau", - "Turkey Standard Time": "Europe/Istanbul", - "Kaliningrad Standard Time": "Europe/Kaliningrad", - "FLE Standard Time": "Europe/Kiev", - "GMT Standard Time": "Europe/London", - "Belarus Standard Time": "Europe/Minsk", - "Russian Standard Time": "Europe/Moscow", - "Romance Standard Time": "Europe/Paris", - "Russia Time Zone 3": "Europe/Samara", - "Saratov Standard Time": "Europe/Saratov", - "Central European Standard Time": "Europe/Warsaw", - "Mauritius Standard Time": "Indian/Mauritius", - "Samoa Standard Time": "Pacific/Apia", - "New Zealand Standard Time": "Pacific/Auckland", - "Bougainville Standard Time": "Pacific/Bougainville", - "Chatham Islands Standard Time": "Pacific/Chatham", - "Easter Island Standard Time": "Pacific/Easter", - "Fiji Standard Time": "Pacific/Fiji", - "Central Pacific Standard Time": "Pacific/Guadalcanal", - "Hawaiian Standard Time": "Pacific/Honolulu", - "Line Islands Standard Time": "Pacific/Kiritimati", - "Marquesas Standard Time": "Pacific/Marquesas", - "Norfolk Standard Time": "Pacific/Norfolk", - "West Pacific Standard Time": "Pacific/Port_Moresby", - "Tonga Standard Time": "Pacific/Tongatapu", + "UTC": "Etc/GMT", + "UTC-11": "Etc/GMT+11", + "Dateline Standard Time": "Etc/GMT+12", + "UTC-02": "Etc/GMT+2", + "UTC-08": "Etc/GMT+8", + "UTC-09": "Etc/GMT+9", + "UTC+12": "Etc/GMT-12", + "UTC+13": "Etc/GMT-13", + "Astrakhan Standard Time": "Europe/Astrakhan", + "W. Europe Standard Time": "Europe/Berlin", + "GTB Standard Time": "Europe/Bucharest", + "Central Europe Standard Time": "Europe/Budapest", + "E. Europe Standard Time": "Europe/Chisinau", + "Turkey Standard Time": "Europe/Istanbul", + "Kaliningrad Standard Time": "Europe/Kaliningrad", + "FLE Standard Time": "Europe/Kiev", + "GMT Standard Time": "Europe/London", + "Belarus Standard Time": "Europe/Minsk", + "Russian Standard Time": "Europe/Moscow", + "Romance Standard Time": "Europe/Paris", + "Russia Time Zone 3": "Europe/Samara", + "Saratov Standard Time": "Europe/Saratov", + "Central European Standard Time": "Europe/Warsaw", + "Mauritius Standard Time": "Indian/Mauritius", + "Samoa Standard Time": "Pacific/Apia", + "New Zealand Standard Time": "Pacific/Auckland", + "Bougainville Standard Time": "Pacific/Bougainville", + "Chatham Islands Standard Time": "Pacific/Chatham", + "Easter Island Standard Time": "Pacific/Easter", + "Fiji Standard Time": "Pacific/Fiji", + "Central Pacific Standard Time": "Pacific/Guadalcanal", + "Hawaiian Standard Time": "Pacific/Honolulu", + "Line Islands Standard Time": "Pacific/Kiritimati", + "Marquesas Standard Time": "Pacific/Marquesas", + "Norfolk Standard Time": "Pacific/Norfolk", + "West Pacific Standard Time": "Pacific/Port_Moresby", + "Tonga Standard Time": "Pacific/Tongatapu", // Additional non-standard timezones "Mexico Standard Time 2": "America/Chihuahua", "E. South America Standard Time 1": "America/Sao_Paulo", @@ -281,6 +281,7 @@ func ParseICalContent(content, url string, maxRepeats int, convertDatesToUTC boo cal.Name = parseICalName(info) cal.Description = parseICalDesc(info) cal.Version = parseICalVersion(info) + cal.Timezone = parseICalTimezone(info) cal.URL = url if fn == nil { @@ -354,7 +355,7 @@ func parseEvents(cal *Calendar, eventsData []string, maxRepeats int) error { for _, eventData := range eventsData { event := NewEvent() - start, err := parseEventDate("DTSTART", eventData) + start, startTz, err := parseEventDate("DTSTART", eventData) if err != nil { if _, ok := err.(*timezoneLocationError); ok { cal.TraceErrFunc(fmt.Errorf("Unmapped timezone location '%s' for iCal '%s'. Falling back to UTC", err.Error(), cal.URL)) @@ -365,7 +366,7 @@ func parseEvents(cal *Calendar, eventsData []string, maxRepeats int) error { } } - end, err := parseEventDate("DTEND", eventData) + end, endTz, err := parseEventDate("DTEND", eventData) if err != nil { if _, ok := err.(*timezoneLocationError); ok { cal.TraceErrFunc(fmt.Errorf("Unmapped timezone location '%s' for iCal '%s'. Falling back to UTC", err.Error(), cal.URL)) @@ -376,6 +377,14 @@ func parseEvents(cal *Calendar, eventsData []string, maxRepeats int) error { } } + if startTz == nil { + startTz = cal.Timezone + } + + if endTz == nil { + endTz = cal.Timezone + } + if end.IsZero() { end = time.Date(start.Year(), start.Month(), start.Day(), 23, 59, 59, 0, start.Location()) } @@ -401,7 +410,7 @@ func parseEvents(cal *Calendar, eventsData []string, maxRepeats int) error { return err } event.ExDates = exclusions - event.RecurrenceID, err = parseEventRecurrenceID(eventData) + event.RecurrenceID, _, err = parseEventRecurrenceID(eventData) if err != nil { return err } @@ -409,6 +418,8 @@ func parseEvents(cal *Calendar, eventsData []string, maxRepeats int) error { event.Location = parseEventLocation(eventData) event.Start = start event.End = end + event.StartTimezone = startTz + event.EndTimezone = endTz event.WholeDayEvent = wholeDay event.Attendees = parseEventAttendees(eventData) event.Organizer = parseEventOrganizer(eventData) @@ -532,16 +543,16 @@ func parseEventModified(eventData string) time.Time { return t } -func parseEventRecurrenceID(eventData string) (time.Time, error) { +func parseEventRecurrenceID(eventData string) (time.Time, *time.Location, error) { rec := eventRecurrenceIDRegex.FindString(eventData) if rec == "" { - return time.Time{}, nil + return time.Time{}, nil, nil } return parseDatetime(rec) } -func parseEventDate(start, eventData string) (time.Time, error) { +func parseEventDate(start, eventData string) (time.Time, *time.Location, error) { ts := eventDateRegex.FindAllString(eventData, -1) t := findWithStart(start, ts) tWholeDay := eventWholeDayRegex.FindString(t) @@ -550,7 +561,7 @@ func parseEventDate(start, eventData string) (time.Time, error) { } if t == "" { - return time.Time{}, nil + return time.Time{}, nil, nil } return parseDatetime(t) @@ -566,7 +577,7 @@ func findWithStart(start string, list []string) string { return "" } -func parseDatetime(data string) (time.Time, error) { +func parseDatetime(data string) (time.Time, *time.Location, error) { data = strings.TrimSpace(data) var dataTz string timeString := data @@ -582,16 +593,16 @@ func parseDatetime(data string) (time.Time, error) { t, err := time.Parse(icsFormat, timeString) if err != nil { - return t, err + return t, nil, err } if strings.Contains(dataTz, "TZID") { loc, err := parseLocation(strings.Split(dataTz, "=")[1]) - return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc), err + return t, loc, err } - return t, nil + return t, nil, nil } func parseLocation(location string) (*time.Location, error) { @@ -629,7 +640,7 @@ func parseLocation(location string) (*time.Location, error) { return timezone, nil } -func parseDate(data string) (time.Time, error) { +func parseDate(data string) (time.Time, *time.Location, error) { return parseDatetime(data + "T000000") } diff --git a/parse_test.go b/parse_test.go index 4c9e909..e0430f9 100644 --- a/parse_test.go +++ b/parse_test.go @@ -125,7 +125,7 @@ func TestParseEventDate(t *testing.T) { expected := time.Date(2015, time.Month(9), 30, 15, 0, 0, 0, loc) dataStart := "DTSTART;TZID=Europe/Madrid:20150930T150000\n" - result, err := parseEventDate("DTSTART", dataStart) + result, _, err := parseEventDate("DTSTART", dataStart) if err != nil { t.FailNow() } @@ -135,7 +135,7 @@ func TestParseEventDate(t *testing.T) { } dataEnd := "DTEND;TZID=Europe/Madrid:20150930T150000\n" - result, err = parseEventDate("DTEND", dataEnd) + result, _, err = parseEventDate("DTEND", dataEnd) if err != nil { t.FailNow() } @@ -153,7 +153,7 @@ func TestParseEventRecurrenceID(t *testing.T) { expected := time.Date(2015, time.Month(10), 13, 15, 0, 0, 0, loc) data := "RECURRENCE-ID;TZID=Europe/Madrid:20151013T150000\n" - result, err := parseEventRecurrenceID(data) + result, _, err := parseEventRecurrenceID(data) if err != nil { t.Error(err) } @@ -190,7 +190,7 @@ END:VEVENT ` func TestParseEventDateWholeDay(t *testing.T) { - tResult, err := parseEventDate("DTSTART", testWholeDayEvent) + tResult, _, err := parseEventDate("DTSTART", testWholeDayEvent) if err != nil { t.Error(err) } From 36637dbda60b70749b62b009b7be55c1612fc59f Mon Sep 17 00:00:00 2001 From: Thales Oliveira Date: Mon, 22 Oct 2018 21:13:35 -0300 Subject: [PATCH 2/2] Properly parse EXDATE's property --- parse.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/parse.go b/parse.go index 092a365..43ee468 100644 --- a/parse.go +++ b/parse.go @@ -662,23 +662,27 @@ func parseExcludedDates(eventData string, convertDatesToUTC bool) ([]time.Time, return nil, err } - dt := strings.TrimSpace(e[2]) - if !strings.Contains(dt, "Z") { - dt += "Z" - } + exDates := strings.Split(e[2], ",") - t, err := time.Parse(icsFormat, dt) - if err != nil { - return nil, err - } + for _, dateStr := range exDates { + dt := strings.TrimSpace(dateStr) + if !strings.Contains(dt, "Z") { + dt += "Z" + } - t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) + t, err := time.Parse(icsFormat, dt) + if err != nil { + continue + } - if convertDatesToUTC { - t = t.UTC() - } + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) - dates = append(dates) + if convertDatesToUTC { + t = t.UTC() + } + + dates = append(dates, t) + } } return dates, nil