Skip to content
Open
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
2 changes: 2 additions & 0 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type Event struct {
Organizer Attendee
WholeDayEvent bool
ExDates []time.Time
StartTimezone *time.Location
EndTimezone *time.Location
}

type byDate []Event
Expand Down
139 changes: 77 additions & 62 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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())
}
Expand All @@ -401,14 +410,16 @@ 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
}

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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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")
}

Expand All @@ -651,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
Expand Down
8 changes: 4 additions & 4 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down