From 320d5fd6096d4aa77f139392ea1e40fd8ec957cd 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 | 48 ++++++++++++++++++++++++++++++++------------- components_test.go | 49 +++++++++++++++++++++++++++++++++++----------- property.go | 11 +++++++++++ 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/components.go b/components.go index b038448..a069987 100644 --- a/components.go +++ b/components.go @@ -30,6 +30,7 @@ func (cb *ComponentBase) UnknownPropertiesIANAProperties() []IANAProperty { func (cb *ComponentBase) SubComponents() []Component { return cb.Components } + func (base ComponentBase) serializeThis(writer io.Writer, componentType string) { fmt.Fprint(writer, "BEGIN:"+componentType, "\r\n") for _, p := range base.Properties { @@ -101,9 +102,7 @@ const ( icalDateFormatLocal = "20060102" ) -var ( - timeStampVariations = regexp.MustCompile("^([0-9]{8})?([TZ])?([0-9]{6})?(Z)?$") -) +var timeStampVariations = regexp.MustCompile("^([0-9]{8})?([TZ])?([0-9]{6})?(Z)?$") func (event *VEvent) SetCreatedTime(t time.Time, props ...PropertyParameter) { event.SetProperty(ComponentPropertyCreated, t.UTC().Format(icalTimestampFormatUtc), props...) @@ -126,8 +125,11 @@ func (event *VEvent) SetStartAt(t time.Time, props ...PropertyParameter) { } func (event *VEvent) SetAllDayStartAt(t time.Time, props ...PropertyParameter) { - props = append(props, WithValue(string(ValueDataTypeDate))) - event.SetProperty(ComponentPropertyDtStart, t.Format(icalDateFormatLocal), props...) + event.SetProperty( + ComponentPropertyDtStart, + t.Format(icalDateFormatLocal), + append(props, WithValue(string(ValueDataTypeDate)))..., + ) } func (event *VEvent) SetEndAt(t time.Time, props ...PropertyParameter) { @@ -135,8 +137,11 @@ func (event *VEvent) SetEndAt(t time.Time, props ...PropertyParameter) { } func (event *VEvent) SetAllDayEndAt(t time.Time, props ...PropertyParameter) { - props = append(props, WithValue(string(ValueDataTypeDate))) - event.SetProperty(ComponentPropertyDtEnd, t.Format(icalDateFormatLocal), props...) + event.SetProperty( + ComponentPropertyDtEnd, + t.Format(icalDateFormatLocal), + append(props, WithValue(string(ValueDataTypeDate)))..., + ) } // SetDuration updates the duration of an event. @@ -145,14 +150,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 d7d8902..a837961 100644 --- a/components_test.go +++ b/components_test.go @@ -64,16 +64,36 @@ func TestSetAllDay(t *testing.T) { date, _ := time.Parse(time.RFC822, time.RFC822) testCases := []struct { - name string - start time.Time - end time.Time - output string + name string + start time.Time + end time.Time + duration time.Duration + output string }{ { - name: "test set duration - start", + name: "test set all day - start", start: date, output: `BEGIN:VEVENT -UID:test-duration +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 @@ -83,12 +103,19 @@ END:VEVENT for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - e := NewEvent("test-duration") - e.SetAllDayStartAt(date) - e.SetAllDayEndAt(date.AddDate(0, 0, 1)) + 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) + } - // we're not testing for encoding here so lets make the actual output line breaks == expected line breaks - text := strings.Replace(e.Serialize(), "\r\n", "\n", -1) + text := strings.ReplaceAll(e.Serialize(), "\r\n", "\n") assert.Equal(t, tc.output, text) }) 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)