From f46725693395fe7c77547828481f999538d08915 Mon Sep 17 00:00:00 2001 From: Dominic Evans Date: Wed, 5 Apr 2023 23:29:08 +0100 Subject: [PATCH] fix: omit zone in "AllDay" event helpers For a date-only event (i.e., an event that lasts for the full day) the iCalendar specification indicates that the value for DTSTART / DTEND should be a DATE https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html > The "VEVENT" is also the calendar component used to specify an > anniversary or daily reminder within a calendar. These events have a > DATE value type for the "DTSTART" property instead of the default value > type of DATE-TIME. If such a "VEVENT" has a "DTEND" property, it MUST be > specified as a DATE value also The DATE format (https://icalendar.org/iCalendar-RFC-5545/3-3-4-date.html) should omit both time and zone/location elements and additionally notes that "The "TZID" property parameter MUST NOT be applied to DATE properties" As per the specification, this PR also adds an explicit "VALUE=DATE" parameter when the AllDay helpers were called, to indicate that the property's default value type has been overridden and the VEVENT is intended to be an all-day event https://icalendar.org/iCalendar-RFC-5545/3-2-20-value-data-types.html Finally the SetDuration call has been updated to preserve the "AllDay" characteristics if the existing start or end has been specified in DATE format, which is also a requirement of the spec. Contributes-to: #55 --- components.go | 41 +++++++++++++++++++++++------- components_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++ property.go | 11 ++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/components.go b/components.go index cb88376..cb59633 100644 --- a/components.go +++ b/components.go @@ -126,7 +126,11 @@ func (event *VEvent) SetStartAt(t time.Time, props ...PropertyParameter) { } func (event *VEvent) SetAllDayStartAt(t time.Time, props ...PropertyParameter) { - event.SetProperty(ComponentPropertyDtStart, t.UTC().Format(icalDateFormatUtc), props...) + event.SetProperty( + ComponentPropertyDtStart, + t.UTC().Format(icalDateFormatLocal), + append(props, WithValue(string(ValueDataTypeDate)))..., + ) } func (event *VEvent) SetEndAt(t time.Time, props ...PropertyParameter) { @@ -134,7 +138,11 @@ func (event *VEvent) SetEndAt(t time.Time, props ...PropertyParameter) { } func (event *VEvent) SetAllDayEndAt(t time.Time, props ...PropertyParameter) { - event.SetProperty(ComponentPropertyDtEnd, t.UTC().Format(icalDateFormatUtc), props...) + event.SetProperty( + ComponentPropertyDtEnd, + t.UTC().Format(icalDateFormatLocal), + append(props, WithValue(string(ValueDataTypeDate)))..., + ) } // SetDuration updates the duration of an event. @@ -143,14 +151,29 @@ func (event *VEvent) SetAllDayEndAt(t time.Time, props ...PropertyParameter) { // // Notice: It will not set the DURATION key of the ics - only DTSTART and DTEND will be affected. func (event *VEvent) SetDuration(d time.Duration) error { - t, err := event.GetStartAt() - if err == nil { - event.SetEndAt(t.Add(d)) - return nil - } else { - t, err = event.GetEndAt() + startProp := event.GetProperty(ComponentPropertyDtStart) + if startProp != nil { + t, err := event.GetStartAt() if err == nil { - event.SetStartAt(t.Add(-d)) + v, _ := startProp.parameterValue(ParameterValue) + if v == string(ValueDataTypeDate) { + event.SetAllDayEndAt(t.Add(d)) + } else { + event.SetEndAt(t.Add(d)) + } + return nil + } + } + endProp := event.GetProperty(ComponentPropertyDtEnd) + if endProp != nil { + t, err := event.GetEndAt() + if err == nil { + v, _ := endProp.parameterValue(ParameterValue) + if v == string(ValueDataTypeDate) { + event.SetAllDayStartAt(t.Add(-d)) + } else { + event.SetStartAt(t.Add(-d)) + } return nil } } diff --git a/components_test.go b/components_test.go index ff7cdf5..d364437 100644 --- a/components_test.go +++ b/components_test.go @@ -8,6 +8,68 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSetAllDay(t *testing.T) { + date, _ := time.Parse(time.RFC822, time.RFC822) + + testCases := []struct { + name string + start time.Time + end time.Time + duration time.Duration + output string + }{ + { + name: "test set all day - start", + start: date, + output: `BEGIN:VEVENT +UID:test-allday +DTSTART;VALUE=DATE:20060102 +END:VEVENT +`, + }, + { + name: "test set all day - end", + end: date, + output: `BEGIN:VEVENT +UID:test-allday +DTEND;VALUE=DATE:20060102 +END:VEVENT +`, + }, + { + name: "test set all day - duration", + start: date, + duration: time.Hour * 24, + output: `BEGIN:VEVENT +UID:test-allday +DTSTART;VALUE=DATE:20060102 +DTEND;VALUE=DATE:20060103 +END:VEVENT +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := NewEvent("test-allday") + if !tc.start.IsZero() { + e.SetAllDayStartAt(tc.start) + } + if !tc.end.IsZero() { + e.SetAllDayEndAt(tc.end) + } + if tc.duration != 0 { + err := e.SetDuration(tc.duration) + assert.NoError(t, err) + } + + text := strings.ReplaceAll(e.Serialize(), "\r\n", "\n") + + assert.Equal(t, tc.output, text) + }) + } +} + func TestSetDuration(t *testing.T) { date, _ := time.Parse(time.RFC822, time.RFC822) duration := time.Duration(float64(time.Hour) * 2) diff --git a/property.go b/property.go index 62418ad..c7051a5 100644 --- a/property.go +++ b/property.go @@ -86,6 +86,17 @@ func trimUT8StringUpTo(maxLength int, s string) string { return s[:length] } +func (property *BaseProperty) parameterValue(param Parameter) (string, error) { + v, ok := property.ICalParameters[string(param)] + if !ok || len(v) == 0 { + return "", fmt.Errorf("parameter %q not found in property", param) + } + if len(v) != 1 { + return "", fmt.Errorf("expected only one value for parameter %q in property, found %d", param, len(v)) + } + return v[0], nil +} + func (property *BaseProperty) serialize(w io.Writer) { b := bytes.NewBufferString("") fmt.Fprint(b, property.IANAToken)