Skip to content

Commit

Permalink
fix: omit zone in "AllDay" event helpers
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dnwe committed Apr 5, 2023
1 parent 19abf92 commit f467256
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 9 deletions.
41 changes: 32 additions & 9 deletions components.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,23 @@ 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) {
event.SetProperty(ComponentPropertyDtEnd, t.UTC().Format(icalTimestampFormatUtc), props...)
}

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.
Expand All @@ -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
}
}
Expand Down
62 changes: 62 additions & 0 deletions components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions property.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f467256

Please sign in to comment.