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 28, 2023
1 parent f69e132 commit 320d5fd
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 25 deletions.
48 changes: 34 additions & 14 deletions components.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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...)
Expand All @@ -126,17 +125,23 @@ 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) {
event.SetProperty(ComponentPropertyDtEnd, t.UTC().Format(icalTimestampFormatUtc), props...)
}

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

Please sign in to comment.