-
Notifications
You must be signed in to change notification settings - Fork 0
/
lilrange.go
134 lines (123 loc) · 3.45 KB
/
lilrange.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package lilrange
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)
type Range struct {
Start time.Time
End time.Time
Duration time.Duration
repr string
}
func (t Range) Within(instant time.Time) bool {
return instant.After(t.Start) && instant.Before(t.End)
}
func (r Range) Next() Range {
return Range{
r.Start.Add(24 * time.Hour),
r.End.Add(24 * time.Hour),
r.Duration,
r.repr,
}
}
func Parse(s string) (*Range, error) {
var tr Range
splitted := strings.Split(s, "-")
if len(splitted) != 2 {
return nil, fmt.Errorf("invalid time range value: %s", s)
}
startHr, startMin, err := extractAndValidate(splitted[0])
if err != nil {
return nil, err
}
endHr, endMin, err := extractAndValidate(splitted[1])
if err != nil {
return nil, err
}
// 0. determine duration
dur, _ := CalculateDurationMinutes(startHr, startMin, endHr, endMin)
// 1. determine if "today's start" has happened
now := time.Now().In(time.UTC)
nowYear, nowMonth, nowDate := now.Date()
todaysStart := time.Date(nowYear, nowMonth, nowDate, startHr, startMin, 0, 0, time.UTC)
endTime := todaysStart.Add(time.Minute * time.Duration(dur))
if now.After(endTime) {
// today's range has ended. Our default behavior is to return the next
// range, e.g. tomorrow's
tr.Start = todaysStart.Add(time.Hour * 24)
tr.End = endTime.Add(time.Hour * 24)
} else {
// Today's range has not ended. We are either a) inside this range, or
// b) it is in the future and has not started. Either way, assign the
// computed times and let the caller decide what to do.
tr.Start = todaysStart
tr.End = endTime
}
tr.Duration = time.Minute * time.Duration(dur)
tr.repr = s
return &tr, nil
}
func CalculateDurationMinutes(startHr, startMin, endHr, endMin int) (int, bool) {
if !between(startHr, 0, 23) || !between(endHr, 0, 23) || !between(startMin, 0, 59) || !between(endMin, 0, 59) {
panic("invalid values for hours and/or minutes")
}
var crossesMidnight bool
if startHr > endHr {
crossesMidnight = true
}
var duration int
if crossesMidnight {
minUntilMidnight := ((24 - startHr) * 60) - startMin
minAfterMidnight := (endHr * 60) + endMin
duration = minUntilMidnight + minAfterMidnight
} else {
duration = ((endHr - startHr) * 60) - startMin + endMin
}
return duration, crossesMidnight
}
func validRune(r rune) bool {
valid := []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
for _, x := range valid {
if x == r {
return true
}
}
return false
}
func extractAndValidate(s string) (int, int, error) {
runes := []rune(s)
if len(runes) != 4 {
return -1, -1, fmt.Errorf("invalid time component: %s", s)
}
for pos, char := range s {
// TODO make this a comparison to range of ASCII bytes rather than another loop
if !validRune(char) {
return -1, -1, fmt.Errorf("invalid char at position %v: %c [%v]",
pos, char, char)
}
}
hr, min := string(runes[0:2]), string(runes[2:4])
// We've asserted that our chars are ASCII 0-9, so Atoi should never fail.
hrInt, err := strconv.Atoi(hr)
if err != nil {
panic("how is this possible")
}
minInt, err := strconv.Atoi(min)
if err != nil {
panic("universe is broken, sorry")
}
if !between(hrInt, 0, 23) {
return -1, -1, errors.New("hour value must be between 0 and 23")
}
if !between(minInt, 0, 59) {
return -1, -1, errors.New("minute value must be between 0 and 59")
}
return hrInt, minInt, nil
}
// x is between this and that
func between(x, this, that int) bool {
return x >= this && x <= that
}