1
2
3
4
5
6
7
8
9
10
11
12
13
14 package timeinterval
15
16 import (
17 "encoding/json"
18 "errors"
19 "fmt"
20 "os"
21 "regexp"
22 "runtime"
23 "strconv"
24 "strings"
25 "time"
26
27 "gopkg.in/yaml.v2"
28 )
29
30
31
32 type TimeInterval struct {
33 Times []TimeRange `yaml:"times,omitempty" json:"times,omitempty"`
34 Weekdays []WeekdayRange `yaml:"weekdays,flow,omitempty" json:"weekdays,omitempty"`
35 DaysOfMonth []DayOfMonthRange `yaml:"days_of_month,flow,omitempty" json:"days_of_month,omitempty"`
36 Months []MonthRange `yaml:"months,flow,omitempty" json:"months,omitempty"`
37 Years []YearRange `yaml:"years,flow,omitempty" json:"years,omitempty"`
38 Location *Location `yaml:"location,flow,omitempty" json:"location,omitempty"`
39 }
40
41
42
43 type TimeRange struct {
44 StartMinute int
45 EndMinute int
46 }
47
48
49 type InclusiveRange struct {
50 Begin int
51 End int
52 }
53
54
55 type WeekdayRange struct {
56 InclusiveRange
57 }
58
59
60 type DayOfMonthRange struct {
61 InclusiveRange
62 }
63
64
65 type MonthRange struct {
66 InclusiveRange
67 }
68
69
70 type YearRange struct {
71 InclusiveRange
72 }
73
74
75 type Location struct {
76 *time.Location
77 }
78
79 type yamlTimeRange struct {
80 StartTime string `yaml:"start_time" json:"start_time"`
81 EndTime string `yaml:"end_time" json:"end_time"`
82 }
83
84
85 type stringableRange interface {
86 setBegin(int)
87 setEnd(int)
88
89 memberFromString(string) (int, error)
90 }
91
92 func (ir *InclusiveRange) setBegin(n int) {
93 ir.Begin = n
94 }
95
96 func (ir *InclusiveRange) setEnd(n int) {
97 ir.End = n
98 }
99
100 func (ir *InclusiveRange) memberFromString(in string) (out int, err error) {
101 out, err = strconv.Atoi(in)
102 if err != nil {
103 return -1, err
104 }
105 return out, nil
106 }
107
108 func (r *WeekdayRange) memberFromString(in string) (out int, err error) {
109 out, ok := daysOfWeek[in]
110 if !ok {
111 return -1, fmt.Errorf("%s is not a valid weekday", in)
112 }
113 return out, nil
114 }
115
116 func (r *MonthRange) memberFromString(in string) (out int, err error) {
117 out, ok := months[in]
118 if !ok {
119 out, err = strconv.Atoi(in)
120 if err != nil {
121 return -1, fmt.Errorf("%s is not a valid month", in)
122 }
123 }
124 return out, nil
125 }
126
127 var daysOfWeek = map[string]int{
128 "sunday": 0,
129 "monday": 1,
130 "tuesday": 2,
131 "wednesday": 3,
132 "thursday": 4,
133 "friday": 5,
134 "saturday": 6,
135 }
136
137 var daysOfWeekInv = map[int]string{
138 0: "sunday",
139 1: "monday",
140 2: "tuesday",
141 3: "wednesday",
142 4: "thursday",
143 5: "friday",
144 6: "saturday",
145 }
146
147 var months = map[string]int{
148 "january": 1,
149 "february": 2,
150 "march": 3,
151 "april": 4,
152 "may": 5,
153 "june": 6,
154 "july": 7,
155 "august": 8,
156 "september": 9,
157 "october": 10,
158 "november": 11,
159 "december": 12,
160 }
161
162 var monthsInv = map[int]string{
163 1: "january",
164 2: "february",
165 3: "march",
166 4: "april",
167 5: "may",
168 6: "june",
169 7: "july",
170 8: "august",
171 9: "september",
172 10: "october",
173 11: "november",
174 12: "december",
175 }
176
177
178 func (tz *Location) UnmarshalYAML(unmarshal func(interface{}) error) error {
179 var str string
180 if err := unmarshal(&str); err != nil {
181 return err
182 }
183
184 loc, err := time.LoadLocation(str)
185 if err != nil {
186 if runtime.GOOS == "windows" {
187 if zoneinfo := os.Getenv("ZONEINFO"); zoneinfo != "" {
188 return fmt.Errorf("%w (ZONEINFO=%q)", err, zoneinfo)
189 }
190 return fmt.Errorf("%w (on Windows platforms, you may have to pass the time zone database using the ZONEINFO environment variable, see https://pkg.go.dev/time#LoadLocation for details)", err)
191 }
192 return err
193 }
194
195 *tz = Location{loc}
196 return nil
197 }
198
199
200
201 func (tz *Location) UnmarshalJSON(in []byte) error {
202 return yaml.Unmarshal(in, tz)
203 }
204
205
206 func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
207 var str string
208 if err := unmarshal(&str); err != nil {
209 return err
210 }
211 if err := stringableRangeFromString(str, r); err != nil {
212 return err
213 }
214 if r.Begin > r.End {
215 return errors.New("start day cannot be before end day")
216 }
217 if r.Begin < 0 || r.Begin > 6 {
218 return fmt.Errorf("%s is not a valid day of the week: out of range", str)
219 }
220 if r.End < 0 || r.End > 6 {
221 return fmt.Errorf("%s is not a valid day of the week: out of range", str)
222 }
223 return nil
224 }
225
226
227
228 func (r *WeekdayRange) UnmarshalJSON(in []byte) error {
229 return yaml.Unmarshal(in, r)
230 }
231
232
233 func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
234 var str string
235 if err := unmarshal(&str); err != nil {
236 return err
237 }
238 if err := stringableRangeFromString(str, r); err != nil {
239 return err
240 }
241
242
243 if r.Begin == 0 || r.Begin < -31 || r.Begin > 31 {
244 return fmt.Errorf("%d is not a valid day of the month: out of range", r.Begin)
245 }
246 if r.End == 0 || r.End < -31 || r.End > 31 {
247 return fmt.Errorf("%d is not a valid day of the month: out of range", r.End)
248 }
249
250 if r.Begin < 0 && r.End > 0 {
251 return fmt.Errorf("end day must be negative if start day is negative")
252 }
253
254
255 checkBegin := r.Begin
256 checkEnd := r.End
257 if r.Begin < 0 {
258 checkBegin = 28 + r.Begin
259 }
260 if r.End < 0 {
261 checkEnd = 28 + r.End
262 }
263 if checkBegin > checkEnd {
264 return fmt.Errorf("end day %d is always before start day %d", r.End, r.Begin)
265 }
266 return nil
267 }
268
269
270
271 func (r *DayOfMonthRange) UnmarshalJSON(in []byte) error {
272 return yaml.Unmarshal(in, r)
273 }
274
275
276 func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
277 var str string
278 if err := unmarshal(&str); err != nil {
279 return err
280 }
281 if err := stringableRangeFromString(str, r); err != nil {
282 return err
283 }
284 if r.Begin > r.End {
285 begin := monthsInv[r.Begin]
286 end := monthsInv[r.End]
287 return fmt.Errorf("end month %s is before start month %s", end, begin)
288 }
289 return nil
290 }
291
292
293
294 func (r *MonthRange) UnmarshalJSON(in []byte) error {
295 return yaml.Unmarshal(in, r)
296 }
297
298
299 func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
300 var str string
301 if err := unmarshal(&str); err != nil {
302 return err
303 }
304 if err := stringableRangeFromString(str, r); err != nil {
305 return err
306 }
307 if r.Begin > r.End {
308 return fmt.Errorf("end year %d is before start year %d", r.End, r.Begin)
309 }
310 return nil
311 }
312
313
314
315 func (r *YearRange) UnmarshalJSON(in []byte) error {
316 return yaml.Unmarshal(in, r)
317 }
318
319
320 func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
321 var y yamlTimeRange
322 if err := unmarshal(&y); err != nil {
323 return err
324 }
325 if y.EndTime == "" || y.StartTime == "" {
326 return errors.New("both start and end times must be provided")
327 }
328 start, err := parseTime(y.StartTime)
329 if err != nil {
330 return err
331 }
332 end, err := parseTime(y.EndTime)
333 if err != nil {
334 return err
335 }
336 if start >= end {
337 return errors.New("start time cannot be equal or greater than end time")
338 }
339 tr.StartMinute, tr.EndMinute = start, end
340 return nil
341 }
342
343
344
345 func (tr *TimeRange) UnmarshalJSON(in []byte) error {
346 return yaml.Unmarshal(in, tr)
347 }
348
349
350 func (r WeekdayRange) MarshalYAML() (interface{}, error) {
351 bytes, err := r.MarshalText()
352 return string(bytes), err
353 }
354
355
356
357
358 func (r WeekdayRange) MarshalText() ([]byte, error) {
359 beginStr, ok := daysOfWeekInv[r.Begin]
360 if !ok {
361 return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin)
362 }
363 if r.Begin == r.End {
364 return []byte(beginStr), nil
365 }
366 endStr, ok := daysOfWeekInv[r.End]
367 if !ok {
368 return nil, fmt.Errorf("unable to convert %d into weekday string", r.End)
369 }
370 rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr)
371 return []byte(rangeStr), nil
372 }
373
374
375 func (tr TimeRange) MarshalYAML() (out interface{}, err error) {
376 startHr := tr.StartMinute / 60
377 endHr := tr.EndMinute / 60
378 startMin := tr.StartMinute % 60
379 endMin := tr.EndMinute % 60
380
381 startStr := fmt.Sprintf("%02d:%02d", startHr, startMin)
382 endStr := fmt.Sprintf("%02d:%02d", endHr, endMin)
383
384 yTr := yamlTimeRange{startStr, endStr}
385 return interface{}(yTr), err
386 }
387
388
389 func (tr TimeRange) MarshalJSON() (out []byte, err error) {
390 startHr := tr.StartMinute / 60
391 endHr := tr.EndMinute / 60
392 startMin := tr.StartMinute % 60
393 endMin := tr.EndMinute % 60
394
395 startStr := fmt.Sprintf("%02d:%02d", startHr, startMin)
396 endStr := fmt.Sprintf("%02d:%02d", endHr, endMin)
397
398 yTr := yamlTimeRange{startStr, endStr}
399 return json.Marshal(yTr)
400 }
401
402
403
404 func (tz Location) MarshalText() ([]byte, error) {
405 if tz.Location == nil {
406 return nil, fmt.Errorf("unable to convert nil location into string")
407 }
408 return []byte(tz.Location.String()), nil
409 }
410
411
412 func (tz Location) MarshalYAML() (interface{}, error) {
413 bytes, err := tz.MarshalText()
414 return string(bytes), err
415 }
416
417
418 func (tz Location) MarshalJSON() (out []byte, err error) {
419 return json.Marshal(tz.String())
420 }
421
422
423
424
425 func (ir InclusiveRange) MarshalText() ([]byte, error) {
426 if ir.Begin == ir.End {
427 return []byte(strconv.Itoa(ir.Begin)), nil
428 }
429 out := fmt.Sprintf("%d:%d", ir.Begin, ir.End)
430 return []byte(out), nil
431 }
432
433
434 func (ir InclusiveRange) MarshalYAML() (interface{}, error) {
435 bytes, err := ir.MarshalText()
436 return string(bytes), err
437 }
438
439
440 const TimeLayout = "15:04"
441
442 var (
443 validTime = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)"
444 validTimeRE = regexp.MustCompile(validTime)
445 )
446
447
448 func daysInMonth(t time.Time) int {
449 monthStart := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
450 monthEnd := monthStart.AddDate(0, 1, 0)
451 diff := monthEnd.Sub(monthStart)
452 return int(diff.Hours() / 24)
453 }
454
455 func clamp(n, min, max int) int {
456 if n <= min {
457 return min
458 }
459 if n >= max {
460 return max
461 }
462 return n
463 }
464
465
466 func (tp TimeInterval) ContainsTime(t time.Time) bool {
467 if tp.Location != nil {
468 t = t.In(tp.Location.Location)
469 }
470 if tp.Times != nil {
471 in := false
472 for _, validMinutes := range tp.Times {
473 if (t.Hour()*60+t.Minute()) >= validMinutes.StartMinute && (t.Hour()*60+t.Minute()) < validMinutes.EndMinute {
474 in = true
475 break
476 }
477 }
478 if !in {
479 return false
480 }
481 }
482 if tp.DaysOfMonth != nil {
483 in := false
484 for _, validDates := range tp.DaysOfMonth {
485 var begin, end int
486 daysInMonth := daysInMonth(t)
487 if validDates.Begin < 0 {
488 begin = daysInMonth + validDates.Begin + 1
489 } else {
490 begin = validDates.Begin
491 }
492 if validDates.End < 0 {
493 end = daysInMonth + validDates.End + 1
494 } else {
495 end = validDates.End
496 }
497
498 if begin > daysInMonth {
499 continue
500 }
501
502 begin = clamp(begin, -1*daysInMonth, daysInMonth)
503 end = clamp(end, -1*daysInMonth, daysInMonth)
504 if t.Day() >= begin && t.Day() <= end {
505 in = true
506 break
507 }
508 }
509 if !in {
510 return false
511 }
512 }
513 if tp.Months != nil {
514 in := false
515 for _, validMonths := range tp.Months {
516 if t.Month() >= time.Month(validMonths.Begin) && t.Month() <= time.Month(validMonths.End) {
517 in = true
518 break
519 }
520 }
521 if !in {
522 return false
523 }
524 }
525 if tp.Weekdays != nil {
526 in := false
527 for _, validDays := range tp.Weekdays {
528 if t.Weekday() >= time.Weekday(validDays.Begin) && t.Weekday() <= time.Weekday(validDays.End) {
529 in = true
530 break
531 }
532 }
533 if !in {
534 return false
535 }
536 }
537 if tp.Years != nil {
538 in := false
539 for _, validYears := range tp.Years {
540 if t.Year() >= validYears.Begin && t.Year() <= validYears.End {
541 in = true
542 break
543 }
544 }
545 if !in {
546 return false
547 }
548 }
549 return true
550 }
551
552
553 func parseTime(in string) (mins int, err error) {
554 if !validTimeRE.MatchString(in) {
555 return 0, fmt.Errorf("couldn't parse timestamp %s, invalid format", in)
556 }
557 timestampComponents := strings.Split(in, ":")
558 if len(timestampComponents) != 2 {
559 return 0, fmt.Errorf("invalid timestamp format: %s", in)
560 }
561 timeStampHours, err := strconv.Atoi(timestampComponents[0])
562 if err != nil {
563 return 0, err
564 }
565 timeStampMinutes, err := strconv.Atoi(timestampComponents[1])
566 if err != nil {
567 return 0, err
568 }
569 if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 {
570 return 0, fmt.Errorf("timestamp %s out of range", in)
571 }
572
573 mins = timeStampHours*60 + timeStampMinutes
574 return mins, nil
575 }
576
577
578 func stringableRangeFromString(in string, r stringableRange) (err error) {
579 in = strings.ToLower(in)
580 if strings.ContainsRune(in, ':') {
581 components := strings.Split(in, ":")
582 if len(components) != 2 {
583 return fmt.Errorf("couldn't parse range %s, invalid format", in)
584 }
585 start, err := r.memberFromString(components[0])
586 if err != nil {
587 return err
588 }
589 End, err := r.memberFromString(components[1])
590 if err != nil {
591 return err
592 }
593 r.setBegin(start)
594 r.setEnd(End)
595 return nil
596 }
597 val, err := r.memberFromString(in)
598 if err != nil {
599 return err
600 }
601 r.setBegin(val)
602 r.setEnd(val)
603 return nil
604 }
605
View as plain text