1
2
3
4
5
6
7
8
9
10
11
12
13
14 package timeinterval
15
16 import (
17 "encoding/json"
18 "reflect"
19 "testing"
20 "time"
21
22 "gopkg.in/yaml.v2"
23 )
24
25 var timeIntervalTestCases = []struct {
26 validTimeStrings []string
27 invalidTimeStrings []string
28 timeInterval TimeInterval
29 }{
30 {
31 timeInterval: TimeInterval{},
32 validTimeStrings: []string{
33 "02 Jan 06 15:04 +0000",
34 "03 Jan 07 10:04 +0000",
35 "04 Jan 06 09:04 +0000",
36 },
37 invalidTimeStrings: []string{},
38 },
39 {
40
41 timeInterval: TimeInterval{
42 Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}},
43 Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
44 },
45 validTimeStrings: []string{
46 "04 May 20 15:04 +0000",
47 "05 May 20 10:04 +0000",
48 "09 Jun 20 09:04 +0000",
49 },
50 invalidTimeStrings: []string{
51 "03 May 20 15:04 +0000",
52 "04 May 20 08:59 +0000",
53 "05 May 20 05:00 +0000",
54 },
55 },
56 {
57
58 timeInterval: TimeInterval{
59 DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: 4, End: 6}}},
60 Months: []MonthRange{{InclusiveRange{Begin: 4, End: 4}}},
61 Years: []YearRange{{InclusiveRange{Begin: 2020, End: 2020}}},
62 },
63 validTimeStrings: []string{
64 "04 Apr 20 15:04 +0000",
65 "05 Apr 20 00:00 +0000",
66 "06 Apr 20 23:05 +0000",
67 },
68 invalidTimeStrings: []string{
69 "03 May 18 15:04 +0000",
70 "03 Apr 20 23:59 +0000",
71 "04 Jun 20 23:59 +0000",
72 "06 Apr 19 23:59 +0000",
73 "07 Apr 20 00:00 +0000",
74 },
75 },
76 {
77
78 timeInterval: TimeInterval{
79 DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -3, End: -1}}},
80 },
81 validTimeStrings: []string{
82 "31 Jan 20 15:04 +0000",
83 "30 Jan 20 15:04 +0000",
84 "29 Jan 20 15:04 +0000",
85 "30 Jun 20 00:00 +0000",
86 "29 Feb 20 23:05 +0000",
87 },
88 invalidTimeStrings: []string{
89 "03 May 18 15:04 +0000",
90 "27 Jan 20 15:04 +0000",
91 "03 Apr 20 23:59 +0000",
92 "04 Jun 20 23:59 +0000",
93 "06 Apr 19 23:59 +0000",
94 "07 Apr 20 00:00 +0000",
95 "01 Mar 20 00:00 +0000",
96 },
97 },
98 {
99
100 timeInterval: TimeInterval{
101 Months: []MonthRange{{InclusiveRange{Begin: 6, End: 6}}},
102 DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -31, End: 31}}},
103 },
104 validTimeStrings: []string{
105 "30 Jun 20 00:00 +0000",
106 "01 Jun 20 00:00 +0000",
107 },
108 invalidTimeStrings: []string{
109 "31 May 20 00:00 +0000",
110 "1 Jul 20 00:00 +0000",
111 },
112 },
113 {
114
115
116 timeInterval: TimeInterval{
117 Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}},
118 Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
119 Location: &Location{mustLoadLocation("Australia/Sydney")},
120 },
121 validTimeStrings: []string{
122 "06 Apr 21 13:00 +1000",
123 },
124 invalidTimeStrings: []string{
125 "06 Apr 21 13:00 +0000",
126 },
127 },
128 {
129
130 timeInterval: TimeInterval{
131 Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}},
132 Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
133 Months: []MonthRange{{InclusiveRange{Begin: 11, End: 11}}},
134 Location: &Location{mustLoadLocation("Australia/Sydney")},
135 },
136 validTimeStrings: []string{
137 "01 Nov 21 09:00 +1100",
138 "31 Oct 21 22:00 +0000",
139 },
140 invalidTimeStrings: []string{
141 "31 Oct 21 21:00 +0000",
142 },
143 },
144 }
145
146 var timeStringTestCases = []struct {
147 timeString string
148 TimeRange TimeRange
149 expectError bool
150 }{
151 {
152 timeString: "{'start_time': '00:00', 'end_time': '24:00'}",
153 TimeRange: TimeRange{StartMinute: 0, EndMinute: 1440},
154 expectError: false,
155 },
156 {
157 timeString: "{'start_time': '01:35', 'end_time': '17:39'}",
158 TimeRange: TimeRange{StartMinute: 95, EndMinute: 1059},
159 expectError: false,
160 },
161 {
162 timeString: "{'start_time': '09:35', 'end_time': '09:39'}",
163 TimeRange: TimeRange{StartMinute: 575, EndMinute: 579},
164 expectError: false,
165 },
166 {
167
168 timeString: "{'start_time': '17:31', 'end_time': '17:31'}",
169 TimeRange: TimeRange{},
170 expectError: true,
171 },
172 {
173
174 timeString: "{'start_time': '12:30', 'end_time': '24:01'}",
175 TimeRange: TimeRange{},
176 expectError: true,
177 },
178 {
179
180 timeString: "{'start_time': '09:30', 'end_time': '07:41'}",
181 TimeRange: TimeRange{},
182 expectError: true,
183 },
184 {
185
186 timeString: "{'start_time': '24:00', 'end_time': '17:41'}",
187 TimeRange: TimeRange{},
188 expectError: true,
189 },
190 {
191
192 timeString: "{'start_time': '14:03'}",
193 TimeRange: TimeRange{},
194 expectError: true,
195 },
196 }
197
198 var yamlUnmarshalTestCases = []struct {
199 in string
200 intervals []TimeInterval
201 contains []string
202 excludes []string
203 expectError bool
204 err string
205 }{
206 {
207
208 in: `
209 ---
210 - weekdays: ['monday:friday']
211 times:
212 - start_time: '09:00'
213 end_time: '17:00'
214 `,
215 intervals: []TimeInterval{
216 {
217 Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
218 Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}},
219 },
220 },
221 contains: []string{
222 "08 Jul 20 09:00 +0000",
223 "08 Jul 20 16:59 +0000",
224 },
225 excludes: []string{
226 "08 Jul 20 05:00 +0000",
227 "08 Jul 20 08:59 +0000",
228 },
229 expectError: false,
230 },
231 {
232
233 in: `
234 ---
235 # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035
236 - weekdays: ['monday:friday', 'sunday']
237 months: ['january:march']
238 days_of_month: ['-7:-1']
239 years: ['2020:2025', '2030:2035']
240 times:
241 - start_time: '09:00'
242 end_time: '17:00'
243 `,
244 intervals: []TimeInterval{
245 {
246 Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 0, End: 0}}},
247 Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}},
248 Months: []MonthRange{{InclusiveRange{1, 3}}},
249 DaysOfMonth: []DayOfMonthRange{{InclusiveRange{-7, -1}}},
250 Years: []YearRange{{InclusiveRange{2020, 2025}}, {InclusiveRange{2030, 2035}}},
251 },
252 },
253 contains: []string{
254 "27 Jan 21 09:00 +0000",
255 "28 Jan 21 16:59 +0000",
256 "29 Jan 21 13:00 +0000",
257 "31 Mar 25 13:00 +0000",
258 "31 Mar 25 13:00 +0000",
259 "31 Jan 35 13:00 +0000",
260 },
261 excludes: []string{
262 "30 Jan 21 13:00 +0000",
263 "01 Apr 21 13:00 +0000",
264 "30 Jan 26 13:00 +0000",
265 "31 Jan 35 17:01 +0000",
266 },
267 expectError: false,
268 },
269 {
270 in: `
271 ---
272 - weekdays: ['monday:friday']
273 times:
274 - start_time: '09:00'
275 end_time: '17:00'`,
276 intervals: []TimeInterval{
277 {
278 Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
279 Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}},
280 },
281 },
282 contains: []string{
283 "01 Apr 21 13:00 +0000",
284 },
285 },
286 {
287
288 in: `
289 ---
290 - times:
291 - start_time: '01:99'
292 end_time: '23:59'`,
293 expectError: true,
294 err: "couldn't parse timestamp 01:99, invalid format",
295 },
296 {
297
298 in: `
299 ---
300 - times:
301 - start_time: '00:00'
302 end_time: '99:99'`,
303 expectError: true,
304 err: "couldn't parse timestamp 99:99, invalid format",
305 },
306 {
307
308 in: `
309 ---
310 - weekdays: ['friday:monday']`,
311 expectError: true,
312 err: "start day cannot be before end day",
313 },
314 {
315
316 in: `
317 ---
318 - weekdays: ['blurgsday:flurgsday']
319 `,
320 expectError: true,
321 err: "blurgsday is not a valid weekday",
322 },
323 {
324
325 in: `
326 ---
327 - weekdays: ['1:3']
328 `,
329 expectError: true,
330 err: "1 is not a valid weekday",
331 },
332 {
333
334 in: `
335 ---
336 - weekdays: ['-2:-1']
337 `,
338 expectError: true,
339 err: "-2 is not a valid weekday",
340 },
341 {
342
343 in: `
344 ---
345 - days_of_month: ['0']
346 `,
347 expectError: true,
348 err: "0 is not a valid day of the month: out of range",
349 },
350 {
351
352 in: `
353 ---
354 - days_of_month: ['-50:-20']
355 `,
356 expectError: true,
357 err: "-50 is not a valid day of the month: out of range",
358 },
359 {
360
361 in: `
362 ---
363 - days_of_month: ['1:50']
364 `,
365 expectError: true,
366 err: "50 is not a valid day of the month: out of range",
367 },
368 {
369
370 in: `
371 ---
372 - days_of_month: ['1:-1']
373 `,
374 intervals: []TimeInterval{
375 {
376 DaysOfMonth: []DayOfMonthRange{{InclusiveRange{1, -1}}},
377 },
378 },
379 expectError: false,
380 },
381 {
382
383 in: `
384 ---
385 - days_of_month: ['-15:5']
386 `,
387 expectError: true,
388 err: "end day must be negative if start day is negative",
389 },
390 {
391
392 in: `
393 ---
394 - days_of_month: ['10:-25']
395 `,
396 expectError: true,
397 err: "end day -25 is always before start day 10",
398 },
399 {
400
401 in: `
402 ---
403 - months: ['January:december']
404 `,
405 expectError: false,
406 intervals: []TimeInterval{
407 {
408 Months: []MonthRange{{InclusiveRange{1, 12}}},
409 },
410 },
411 },
412 {
413
414 in: `
415 ---
416 - years: ['2020:2022']
417 location: 'Australia/Sydney'
418 `,
419 expectError: false,
420 intervals: []TimeInterval{
421 {
422 Years: []YearRange{{InclusiveRange{2020, 2022}}},
423 Location: &Location{mustLoadLocation("Australia/Sydney")},
424 },
425 },
426 },
427 {
428
429 in: `
430 ---
431 - months: ['martius:june']
432 `,
433 expectError: true,
434 err: "martius is not a valid month",
435 },
436 {
437
438 in: `
439 ---
440 - months: ['march:junius']
441 `,
442 expectError: true,
443 err: "junius is not a valid month",
444 },
445 {
446
447 in: `
448 ---
449 - months: ['december:january']
450 `,
451 expectError: true,
452 err: "end month january is before start month december",
453 },
454 {
455
456 in: `
457 ---
458 - years: ['2022:2020']
459 `,
460 expectError: true,
461 err: "end year 2020 is before start year 2022",
462 },
463 }
464
465 func TestYamlUnmarshal(t *testing.T) {
466 for _, tc := range yamlUnmarshalTestCases {
467 var ti []TimeInterval
468 err := yaml.Unmarshal([]byte(tc.in), &ti)
469 if err != nil && !tc.expectError {
470 t.Errorf("Received unexpected error: %v when parsing %v", err, tc.in)
471 } else if err == nil && tc.expectError {
472 t.Errorf("Expected error when unmarshalling %s but didn't receive one", tc.in)
473 } else if err != nil && tc.expectError {
474 if err.Error() != tc.err {
475 t.Errorf("Incorrect error: Want %s, got %s", tc.err, err.Error())
476 }
477 continue
478 }
479 if !reflect.DeepEqual(ti, tc.intervals) {
480 t.Errorf("Error unmarshalling %s: Want %+v, got %+v", tc.in, tc.intervals, ti)
481 }
482 for _, ts := range tc.contains {
483 _t, _ := time.Parse(time.RFC822Z, ts)
484 isContained := false
485 for _, interval := range ti {
486 if interval.ContainsTime(_t) {
487 isContained = true
488 }
489 }
490 if !isContained {
491 t.Errorf("Expected intervals to contain time %s", _t)
492 }
493 }
494 for _, ts := range tc.excludes {
495 _t, _ := time.Parse(time.RFC822Z, ts)
496 isContained := false
497 for _, interval := range ti {
498 if interval.ContainsTime(_t) {
499 isContained = true
500 }
501 }
502 if isContained {
503 t.Errorf("Expected intervals to exclude time %s", _t)
504 }
505 }
506 }
507 }
508
509 func TestContainsTime(t *testing.T) {
510 for _, tc := range timeIntervalTestCases {
511 for _, ts := range tc.validTimeStrings {
512 _t, _ := time.Parse(time.RFC822Z, ts)
513 if !tc.timeInterval.ContainsTime(_t) {
514 t.Errorf("Expected period %+v to contain %+v", tc.timeInterval, _t)
515 }
516 }
517 for _, ts := range tc.invalidTimeStrings {
518 _t, _ := time.Parse(time.RFC822Z, ts)
519 if tc.timeInterval.ContainsTime(_t) {
520 t.Errorf("Period %+v not expected to contain %+v", tc.timeInterval, _t)
521 }
522 }
523 }
524 }
525
526 func TestParseTimeString(t *testing.T) {
527 for _, tc := range timeStringTestCases {
528 var tr TimeRange
529 err := yaml.Unmarshal([]byte(tc.timeString), &tr)
530 if err != nil && !tc.expectError {
531 t.Errorf("Received unexpected error: %v when parsing %v", err, tc.timeString)
532 } else if err == nil && tc.expectError {
533 t.Errorf("Expected error for invalid string %s but didn't receive one", tc.timeString)
534 } else if !reflect.DeepEqual(tr, tc.TimeRange) {
535 t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.timeString, tc.TimeRange, tr)
536 }
537 }
538 }
539
540 func TestYamlMarshal(t *testing.T) {
541 for _, tc := range yamlUnmarshalTestCases {
542 if tc.expectError {
543 continue
544 }
545 var ti []TimeInterval
546 err := yaml.Unmarshal([]byte(tc.in), &ti)
547 if err != nil {
548 t.Error(err)
549 }
550 out, err := yaml.Marshal(&ti)
551 if err != nil {
552 t.Error(err)
553 }
554 var ti2 []TimeInterval
555 yaml.Unmarshal(out, &ti2)
556 if !reflect.DeepEqual(ti, ti2) {
557 t.Errorf("Re-marshalling %s produced a different TimeInterval.", tc.in)
558 }
559 }
560 }
561
562
563
564 func TestJsonMarshal(t *testing.T) {
565 for _, tc := range yamlUnmarshalTestCases {
566 if tc.expectError {
567 continue
568 }
569 var ti []TimeInterval
570 err := yaml.Unmarshal([]byte(tc.in), &ti)
571 if err != nil {
572 t.Error(err)
573 }
574 out, err := json.Marshal(&ti)
575 if err != nil {
576 t.Error(err)
577 }
578 var ti2 []TimeInterval
579 json.Unmarshal(out, &ti2)
580 if !reflect.DeepEqual(ti, ti2) {
581 t.Errorf("Re-marshalling %s produced a different TimeInterval. Used:\n%s and got:\n%v", tc.in, out, ti2)
582 }
583 }
584 }
585
586 var completeTestCases = []struct {
587 in string
588 contains []string
589 excludes []string
590 }{
591 {
592 in: `
593 ---
594 weekdays: ['monday:wednesday', 'saturday', 'sunday']
595 times:
596 - start_time: '13:00'
597 end_time: '15:00'
598 days_of_month: ['1', '10', '20:-1']
599 years: ['2020:2023']
600 months: ['january:march']
601 `,
602 contains: []string{
603 "10 Jan 21 13:00 +0000",
604 "30 Jan 21 14:24 +0000",
605 },
606 excludes: []string{
607 "09 Jan 21 13:00 +0000",
608 "20 Jan 21 12:59 +0000",
609 "02 Feb 21 13:00 +0000",
610 },
611 },
612 {
613
614 in: `
615 ---
616 days_of_month: ['30:31']
617 years: ['2020:2023']
618 months: ['february']
619 `,
620 excludes: []string{
621 "28 Feb 21 13:00 +0000",
622 },
623 },
624 }
625
626
627 func TestTimeIntervalComplete(t *testing.T) {
628 for _, tc := range completeTestCases {
629 var ti TimeInterval
630 if err := yaml.Unmarshal([]byte(tc.in), &ti); err != nil {
631 t.Error(err)
632 }
633 for _, ts := range tc.contains {
634 tt, err := time.Parse(time.RFC822Z, ts)
635 if err != nil {
636 t.Error(err)
637 }
638 if !ti.ContainsTime(tt) {
639 t.Errorf("Expected %s to contain %s", tc.in, ts)
640 }
641 }
642 for _, ts := range tc.excludes {
643 tt, err := time.Parse(time.RFC822Z, ts)
644 if err != nil {
645 t.Error(err)
646 }
647 if ti.ContainsTime(tt) {
648 t.Errorf("Expected %s to exclude %s", tc.in, ts)
649 }
650 }
651 }
652 }
653
654
655 func mustLoadLocation(name string) *time.Location {
656 loc, err := time.LoadLocation(name)
657 if err != nil {
658 panic(err)
659 }
660 return loc
661 }
662
View as plain text