Skip to content

Commit 6f7e914

Browse files
committed
implementing InvalidBehavior (SKIP) in RFC 7529
Also enforcing stricter ranges on numeric inputs.
1 parent a58868b commit 6f7e914

10 files changed

+431
-44
lines changed

expansions.go

+60-12
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func expandByWeekdays(tt []time.Time, weekStart time.Weekday, weekdays ...Qualif
7777

7878
return e
7979
}
80+
8081
func expandByMonthDays(tt []time.Time, monthdays ...int) []time.Time {
8182
if len(monthdays) == 0 {
8283
return tt
@@ -92,42 +93,89 @@ func expandByMonthDays(tt []time.Time, monthdays ...int) []time.Time {
9293
return e
9394
}
9495

95-
func expandByYearDays(tt []time.Time, yeardays ...int) []time.Time {
96+
func expandByYearDays(tt []time.Time, ib InvalidBehavior, yeardays ...int) []time.Time {
9697
if len(yeardays) == 0 {
9798
return tt
9899
}
99100

100101
e := make([]time.Time, 0, len(tt)*len(yeardays))
101102
for _, t := range tt {
102103
yearStart := time.Date(t.Year(), time.January, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
104+
startYear := yearStart.Year()
103105

104106
for _, yd := range yeardays {
105-
e = append(e, yearStart.AddDate(0, 0, yd))
107+
added := yearStart.AddDate(0, 0, yd-1) // subtract one because we start on the 1st, so if we want yearday 1, we actually want to advance 0.
108+
if added.Year() != startYear {
109+
switch ib {
110+
case OmitInvalid:
111+
// do nothing
112+
case NextInvalid:
113+
e = append(e, added)
114+
case PrevInvalid:
115+
e = append(e, added.AddDate(0, 0, -1))
116+
}
117+
} else {
118+
e = append(e, added)
119+
}
106120
}
107121
}
108122

109123
return e
110124
}
111125

112-
func expandByWeekNumbers(tt []time.Time, weekStarts time.Weekday, weekNumbers ...int) []time.Time {
126+
func expandByWeekNumbers(tt []time.Time, ib InvalidBehavior, weekStarts time.Weekday, byWeekdays []time.Weekday, weekNumbers ...int) []time.Time {
113127
if len(weekNumbers) == 0 {
114128
return tt
115129
}
116130

117131
e := make([]time.Time, 0, len(tt)*len(weekNumbers))
118132
for _, t := range tt {
119-
yearStart := time.Date(t.Year(), time.January, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
120-
yearStart = forwardToWeekday(yearStart, t.Weekday())
133+
ys := yearStart(t, weekStarts)
134+
135+
byWeekdays := byWeekdays
136+
if len(byWeekdays) == 0 {
137+
// NOTE: the spec is not 100% clear on what to do in this case.
138+
// rrule.js, for instance, will default to returning the full
139+
// week. lib-recur seems to copy the weekday from the input
140+
// time. I'm going with the latter, since it seems more consistent
141+
// with the behavior you'd get on a BYMONTH clause.
142+
byWeekdays = []time.Weekday{t.Weekday()}
143+
}
121144

122145
for _, w := range weekNumbers {
123-
e = append(e, yearStart.AddDate(0, 0, (w-1)*7))
146+
ws := ys.AddDate(0, 0, (w-1)*7)
147+
148+
if weekYearStart := yearStart(ws, weekStarts); weekYearStart.Year() != ys.Year() {
149+
// check that the week we generated is still within the proper
150+
// year, or if it ran over because the year did not have enough
151+
// weeks
152+
153+
nextYearStart := yearStart(ys.AddDate(1, 0, 0), weekStarts)
154+
switch ib {
155+
case OmitInvalid:
156+
// do nothing
157+
case NextInvalid:
158+
for _, wd := range byWeekdays {
159+
e = append(e, forwardToWeekday(nextYearStart, wd))
160+
}
161+
case PrevInvalid:
162+
for _, wd := range byWeekdays {
163+
e = append(e, backToWeekday(nextYearStart, wd))
164+
}
165+
}
166+
continue
167+
}
168+
169+
for _, wd := range byWeekdays {
170+
e = append(e, forwardToWeekday(ws, wd))
171+
}
124172
}
125173
}
126174

127175
return e
128176
}
129177

130-
func expandByMonths(tt []time.Time, ib invalidBehavior, months ...time.Month) []time.Time {
178+
func expandByMonths(tt []time.Time, ib InvalidBehavior, months ...time.Month) []time.Time {
131179
if len(months) == 0 {
132180
return tt
133181
}
@@ -138,13 +186,13 @@ func expandByMonths(tt []time.Time, ib invalidBehavior, months ...time.Month) []
138186
set := time.Date(t.Year(), m, t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
139187
if set.Month() != m {
140188
switch ib {
141-
case prevInvalid:
189+
case PrevInvalid:
142190
set = time.Date(t.Year(), t.Month()+1, -1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
143191
e = append(e, set)
144-
case nextInvalid:
192+
case NextInvalid:
145193
set = time.Date(t.Year(), t.Month()+1, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
146194
e = append(e, set)
147-
case omitInvalid:
195+
case OmitInvalid:
148196
// do nothing
149197
}
150198
} else {
@@ -160,7 +208,7 @@ func expandByMonths(tt []time.Time, ib invalidBehavior, months ...time.Month) []
160208
// bySetPos is not nil, it is assumed tt is the full set of instances within the
161209
// monthly iteration, and only the instances matching the posisions of bySetPos
162210
// are returned. This is an optimization.
163-
func expandMonthByWeekdays(tt []time.Time, ib invalidBehavior, bySetPos []int, weekdays ...QualifiedWeekday) []time.Time {
211+
func expandMonthByWeekdays(tt []time.Time, ib InvalidBehavior, bySetPos []int, weekdays ...QualifiedWeekday) []time.Time {
164212
if len(weekdays) == 0 {
165213
return tt
166214
}
@@ -173,7 +221,7 @@ func expandMonthByWeekdays(tt []time.Time, ib invalidBehavior, bySetPos []int, w
173221
return e
174222
}
175223

176-
func expandYearByWeekdays(tt []time.Time, ib invalidBehavior, weekdays ...QualifiedWeekday) []time.Time {
224+
func expandYearByWeekdays(tt []time.Time, ib InvalidBehavior, weekdays ...QualifiedWeekday) []time.Time {
177225
if len(weekdays) == 0 {
178226
return tt
179227
}

go.mod

+8
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
module github.com/stephens2424/rrule
2+
3+
go 1.12
4+
5+
require (
6+
github.com/stretchr/testify v1.3.0
7+
github.com/teambition/rrule-go v1.2.3
8+
golang.org/x/tools v0.0.0-20190228203856-589c23e65e65 // indirect
9+
)

invalidBehavior.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package rrule
22

3-
type invalidBehavior int
3+
// InvalidBehavior specifies how to behave when a pattern generates a date that
4+
// wouldn't exist, like February 31st.
5+
type InvalidBehavior int
46

57
const (
6-
omitInvalid invalidBehavior = iota
7-
nextInvalid
8-
prevInvalid
8+
// OmitInvalid skips invalid dates. This is the only choice for RFC
9+
// 5545 and RFC 2445.
10+
OmitInvalid InvalidBehavior = iota
11+
12+
// NextInvalid chooses the next valid date. So if February 31st were generated,
13+
// March 1st would be used.
14+
NextInvalid
15+
16+
// PrevInvalid chooses the previously valid date. So if February 31st were generated,
17+
// the result would be February 28th (or 29th on a leap year).
18+
PrevInvalid
919
)

parse.go

+59-9
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,19 @@ func ParseRRule(str string) (RRule, error) {
149149
}
150150
rrule.Interval = i
151151
case "BYSECOND":
152-
ints, err := parseInts(value)
152+
ints, err := parseInts(value, 0, 60, true)
153153
if err != nil {
154154
return rrule, err
155155
}
156156
rrule.BySeconds = ints
157157
case "BYMINUTE":
158-
ints, err := parseInts(value)
158+
ints, err := parseInts(value, 0, 59, true)
159159
if err != nil {
160160
return rrule, err
161161
}
162162
rrule.ByMinutes = ints
163163
case "BYHOUR":
164-
ints, err := parseInts(value)
164+
ints, err := parseInts(value, 0, 23, true)
165165
if err != nil {
166166
return rrule, err
167167
}
@@ -173,19 +173,19 @@ func ParseRRule(str string) (RRule, error) {
173173
}
174174
rrule.ByWeekdays = wds
175175
case "BYMONTHDAY":
176-
ints, err := parseInts(value)
176+
ints, err := parseInts(value, -31, 31, false)
177177
if err != nil {
178178
return rrule, err
179179
}
180180
rrule.ByMonthDays = ints
181181
case "BYYEARDAY":
182-
ints, err := parseInts(value)
182+
ints, err := parseInts(value, -366, 366, true)
183183
if err != nil {
184184
return rrule, err
185185
}
186186
rrule.ByYearDays = ints
187187
case "BYWEEKNO":
188-
ints, err := parseInts(value)
188+
ints, err := parseInts(value, -53, 53, false)
189189
if err != nil {
190190
return rrule, err
191191
}
@@ -197,7 +197,7 @@ func ParseRRule(str string) (RRule, error) {
197197
}
198198
rrule.ByMonths = months
199199
case "BYSETPOS":
200-
ints, err := parseInts(value)
200+
ints, err := parseInts(value, -366, 366, false)
201201
if err != nil {
202202
return rrule, err
203203
}
@@ -208,6 +208,18 @@ func ParseRRule(str string) (RRule, error) {
208208
return rrule, err
209209
}
210210
rrule.WeekStart = &wd
211+
case "SKIP":
212+
skip, err := parseSkip(value)
213+
if err != nil {
214+
return rrule, err
215+
}
216+
rrule.InvalidBehavior = skip
217+
case "RSCALE":
218+
err := parseRScale(value)
219+
if err != nil {
220+
return rrule, err
221+
}
222+
211223
default:
212224
return rrule, fmt.Errorf("%q is not a supported RRULE part", directive)
213225
}
@@ -217,18 +229,34 @@ func ParseRRule(str string) (RRule, error) {
217229
return rrule, err
218230
}
219231

220-
func parseInts(str string) ([]int, error) {
232+
func parseInts(str string, min, max int, allowZero bool) ([]int, error) {
221233
if len(str) == 0 {
222234
return nil, nil
223235
}
224236
var err error
225237
parts := strings.Split(str, ",")
226238
ints := make([]int, len(parts))
227239
for i, p := range parts {
228-
ints[i], err = strconv.Atoi(p)
240+
var currentInt int
241+
242+
currentInt, err = strconv.Atoi(p)
229243
if err != nil {
230244
return nil, err
231245
}
246+
247+
if currentInt == 0 && !allowZero {
248+
return nil, fmt.Errorf("zero is not valid")
249+
}
250+
251+
if currentInt < min {
252+
return nil, fmt.Errorf("%d is below minimum %d", currentInt, min)
253+
}
254+
255+
if currentInt > max {
256+
return nil, fmt.Errorf("%d is above maximum %d", currentInt, max)
257+
}
258+
259+
ints[i] = currentInt
232260
}
233261

234262
return ints, nil
@@ -331,3 +359,25 @@ func strToFreq(str string) (Frequency, error) {
331359
return Yearly, fmt.Errorf("frequency %q is not valid", str)
332360
}
333361
}
362+
363+
func parseSkip(str string) (InvalidBehavior, error) {
364+
switch strings.ToLower(str) {
365+
case "omit":
366+
return OmitInvalid, nil
367+
case "backward":
368+
return PrevInvalid, nil
369+
case "forward":
370+
return NextInvalid, nil
371+
}
372+
373+
return OmitInvalid, fmt.Errorf("skip value %v is not valid", str)
374+
}
375+
376+
func parseRScale(str string) error {
377+
switch strings.ToLower(str) {
378+
case "gregorian", "gregory":
379+
return nil
380+
default:
381+
return fmt.Errorf("invalid rscale %q: only gregorian is supported", str)
382+
}
383+
}

0 commit comments

Comments
 (0)