Skip to content

Commit 5ac7514

Browse files
committed
refactor ical importer for rrule repetition
1 parent ef96be5 commit 5ac7514

2 files changed

Lines changed: 37 additions & 72 deletions

File tree

pkg/core/ical.go

Lines changed: 15 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package core
22

33
import (
44
"crypto/sha256"
5-
"errors"
65
"fmt"
76
"io"
87
"time"
98

109
ics "github.com/arran4/golang-ical"
1110
"github.com/google/uuid"
11+
rrule "github.com/teambition/rrule-go"
1212
)
1313

14+
// parseICal parses ical events from r to []Event.
15+
// calendar is copied to each event and used to scope deterministic IDs.
16+
// When stableIDs is false, each event gets a random ID.
1417
func parseICal(r io.Reader, calendar string, stableIDs bool) ([]Event, error) {
1518
cal, err := ics.ParseCalendar(r)
1619
if err != nil {
@@ -25,7 +28,7 @@ func parseICal(r io.Reader, calendar string, stableIDs bool) ([]Event, error) {
2528
return nil, fmt.Errorf("import event %d: %w", i+1, err)
2629
}
2730
if stableIDs {
28-
event.Id = icalEventID(calendar, icalText(source, ics.ComponentPropertyUniqueId), i, event)
31+
event.Id = icalEventID(calendar, source.Id(), i, event)
2932
}
3033
if err := event.Validate(); err != nil {
3134
return nil, fmt.Errorf("import event %d: %w", i+1, err)
@@ -47,7 +50,7 @@ func parseICalEvent(source *ics.VEvent, calendar string) (Event, error) {
4750
return Event{}, fmt.Errorf("read DTEND: %w", err)
4851
}
4952

50-
repeat, err := parseICalRepetition(source)
53+
repeat, err := parseICalRRule(source, from)
5154
if err != nil {
5255
return Event{}, err
5356
}
@@ -63,63 +66,18 @@ func parseICalEvent(source *ics.VEvent, calendar string) (Event, error) {
6366
}, nil
6467
}
6568

66-
func parseICalRepetition(event *ics.VEvent) (*Repetition, error) {
67-
if event.HasProperty(ics.ComponentPropertyRdate) ||
68-
event.HasProperty(ics.ComponentPropertyExdate) ||
69-
event.HasProperty(ics.ComponentPropertyExrule) ||
70-
event.HasProperty(ics.ComponentPropertyRecurrenceId) {
71-
return nil, errors.New("recurrence dates and exceptions are not supported")
72-
}
73-
74-
rules, err := event.GetRRules()
75-
if err != nil {
76-
return nil, err
77-
}
78-
if len(rules) == 0 {
69+
func parseICalRRule(event *ics.VEvent, start time.Time) (*rrule.Set, error) {
70+
value := icalText(event, ics.ComponentPropertyRrule)
71+
if value == "" {
7972
return nil, nil
8073
}
81-
if len(rules) != 1 {
82-
return nil, errors.New("multiple recurrence rules are not supported")
83-
}
84-
85-
rule := rules[0]
86-
if hasICalModifiers(rule) {
87-
return nil, errors.New("recurrence modifiers are not supported")
88-
}
8974

90-
frequency, err := parseICalFrequency(rule.Freq)
75+
option, err := rrule.StrToROptionInLocation(value, start.Location())
9176
if err != nil {
92-
return nil, err
93-
}
94-
95-
return &Repetition{
96-
Frequency: frequency,
97-
Interval: rule.Interval,
98-
Until: rule.Until,
99-
Count: rule.Count,
100-
}, nil
101-
}
102-
103-
func parseICalFrequency(frequency ics.Frequency) (Freq, error) {
104-
switch frequency {
105-
case ics.FrequencyDaily:
106-
return Day, nil
107-
case ics.FrequencyWeekly:
108-
return Week, nil
109-
case ics.FrequencyMonthly:
110-
return Month, nil
111-
case ics.FrequencyYearly:
112-
return Year, nil
113-
default:
114-
return Invalid, fmt.Errorf("unsupported recurrence frequency %q", frequency)
77+
return nil, fmt.Errorf("parse RRULE: %w", err)
11578
}
116-
}
117-
118-
func hasICalModifiers(rule *ics.RecurrenceRule) bool {
119-
return len(rule.BySecond)+len(rule.ByMinute)+len(rule.ByHour)+
120-
len(rule.ByDay)+len(rule.ByMonthDay)+len(rule.ByYearDay)+
121-
len(rule.ByWeekNo)+len(rule.ByMonth)+len(rule.BySetPos) > 0 ||
122-
rule.Wkst != ""
79+
option.Dtstart = start
80+
return newRecurrence(*option, nil)
12381
}
12482

12583
func icalText(event *ics.VEvent, property ics.ComponentProperty) string {
@@ -132,13 +90,8 @@ func icalText(event *ics.VEvent, property ics.ComponentProperty) string {
13290

13391
func icalEventID(calendar, uid string, index int, event Event) uuid.UUID {
13492
if uid == "" {
93+
// fallback it uid is missing
13594
uid = fmt.Sprintf("%d\x00%s\x00%s", index, event.Title, event.From.Format(time.RFC3339Nano))
13695
}
137-
sum := sha256.Sum256([]byte(calendar + "\x00" + uid))
138-
139-
var id uuid.UUID
140-
copy(id[:], sum[:16])
141-
id[6] = id[6]&0x0f | 0x80
142-
id[8] = id[8]&0x3f | 0x80
143-
return id
96+
return uuid.NewHash(sha256.New(), uuid.Nil, []byte(calendar+"\x00"+uid), 8)
14497
}

pkg/core/ical_test.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"strings"
55
"testing"
66
"time"
7+
8+
rrule "github.com/teambition/rrule-go"
79
)
810

911
func TestParseICal(t *testing.T) {
@@ -58,18 +60,23 @@ END:VCALENDAR`
5860
if second.Repeat == nil {
5961
t.Fatal("Repeat is nil")
6062
}
61-
if second.Repeat.Frequency != Week {
62-
t.Errorf("Frequency = %v, want %v", second.Repeat.Frequency, Week)
63+
rule := second.Repeat.GetRRule()
64+
if rule == nil {
65+
t.Fatal("RRULE is nil")
66+
}
67+
option := rule.OrigOptions
68+
if option.Freq != rrule.WEEKLY {
69+
t.Errorf("Frequency = %v, want %v", option.Freq, rrule.WEEKLY)
6370
}
64-
if second.Repeat.Interval != 2 {
65-
t.Errorf("Interval = %d, want 2", second.Repeat.Interval)
71+
if option.Interval != 2 {
72+
t.Errorf("Interval = %d, want 2", option.Interval)
6673
}
67-
if second.Repeat.Count != 4 {
68-
t.Errorf("Count = %d, want 4", second.Repeat.Count)
74+
if option.Count != 4 {
75+
t.Errorf("Count = %d, want 4", option.Count)
6976
}
7077
}
7178

72-
func TestParseICalRejectsUnsupportedRecurrence(t *testing.T) {
79+
func TestParseICalSupportsRecurrenceModifiers(t *testing.T) {
7380
input := `BEGIN:VCALENDAR
7481
VERSION:2.0
7582
PRODID:-//git-calendar//test//EN
@@ -82,9 +89,14 @@ RRULE:FREQ=WEEKLY;COUNT=4;BYDAY=MO,WE
8289
END:VEVENT
8390
END:VCALENDAR`
8491

85-
_, err := parseICal(strings.NewReader(input), "Work", false)
86-
if err == nil || !strings.Contains(err.Error(), "recurrence modifiers are not supported") {
87-
t.Fatalf("error = %v, want unsupported recurrence modifier error", err)
92+
events, err := parseICal(strings.NewReader(input), "Work", false)
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
97+
weekdays := events[0].Repeat.GetRRule().OrigOptions.Byweekday
98+
if len(weekdays) != 2 || weekdays[0].String() != "MO" || weekdays[1].String() != "WE" {
99+
t.Fatalf("Byweekday = %v, want [MO WE]", weekdays)
88100
}
89101
}
90102

0 commit comments

Comments
 (0)