/
clock.go
200 lines (169 loc) · 5.02 KB
/
clock.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package timeutil
import (
"database/sql/driver"
"fmt"
"io"
"sort"
"time"
"github.com/pkg/errors"
)
// IsDST will return true if there is a DST change within 24-hours AFTER t.
//
// If so, the clock-time and amount of change is calculated.
func IsDST(t time.Time) (dst bool, at, change Clock) {
next := t.Add(24 * time.Hour)
_, oldOffset := t.Zone()
_, newOffset := next.Zone()
if oldOffset == newOffset {
return false, 0, 0
}
mins := sort.Search(int(24*time.Hour/time.Minute), func(min int) bool {
_, n := t.Add(time.Duration(min) * time.Minute).Zone()
return n == newOffset
})
return true, NewClock(0, mins), Clock(time.Duration(newOffset-oldOffset) * time.Second)
}
// Days will return the number of whole days and the remainder Clock value.
func (c Clock) Days() (int, Clock) {
days := time.Duration(c) / (24 * time.Hour)
rem := time.Duration(c) % (24 * time.Hour)
if rem < 0 {
days--
rem += 24 * time.Hour
}
return int(days), Clock(rem)
}
// FirstOfDay will return the first timestamp where the time matches
// the clock value, or the first instant after, if it does not exist.
func (c Clock) FirstOfDay(t time.Time) time.Time {
y, m, d := t.Date()
t = time.Date(y, m, d, 0, 0, 0, 0, t.Location())
isDST, dstAt, dstChange := IsDST(t)
if !isDST || c < dstAt {
// if we spring forward, DST won't happen yet
// if we fall back, we'll land on the 'first' instance
return t.Add(time.Duration(c))
}
// c >= dstAt
if dstChange > 0 {
if c < dstAt+dstChange {
// e.g. 2:30AM when we go from 2AM->3AM, so return 3AM.
return t.Add(time.Duration(dstAt))
}
// spring forward, so lose the amount of time
return t.Add(time.Duration(c - dstChange))
}
// falls back and the target time is >= the fallback time
// so add extra clock time
return t.Add(time.Duration(c + -dstChange))
}
// LastOfDay will return the last timestamp where the time matches
// the clock value, or the first instant after, if it does not exist.
func (c Clock) LastOfDay(t time.Time) time.Time {
y, m, d := t.Date()
t = time.Date(y, m, d, 0, 0, 0, 0, t.Location())
isDST, dstAt, dstChange := IsDST(t)
if !isDST || (dstChange > 0 && c < dstAt) {
return t.Add(time.Duration(c))
}
if dstChange > 0 {
if c < dstAt+dstChange {
// e.g. 2:30AM when we go from 2AM->3AM, so return 3AM.
return t.Add(time.Duration(dstAt))
}
// >=dstAt so subtract the change
return t.Add(time.Duration(c - dstChange))
}
dstRepeatAt := dstAt + dstChange
if c < dstRepeatAt {
return t.Add(time.Duration(c))
}
return t.Add(time.Duration(c + -dstChange))
}
// Clock represents wall-clock time. It is a duration since midnight.
type Clock time.Duration
// ParseClock will return a new Clock value given a value in the format of '15:04' or '15:04:05'.
// The resulting value will be truncated to the minute.
func ParseClock(value string) (Clock, error) {
var h, m int
var s float64
n, err := fmt.Sscanf(value, "%d:%d:%f", &h, &m, &s)
if n == 2 && errors.Is(err, io.ErrUnexpectedEOF) {
err = nil
}
if err != nil {
return 0, err
}
if n < 2 {
return 0, errors.New("invalid time format")
}
if n == 3 && (s < 0 || s >= 60) {
return 0, errors.New("invalid seconds value")
}
if h < 0 || h > 23 {
return 0, errors.New("invalid hours value")
}
if m < 0 || m > 59 {
return 0, errors.New("invalid minutes value")
}
return NewClock(h, m), nil
}
// Is returns true if t represents the same clock time.
func (c Clock) Is(t time.Time) bool {
h, m, _ := t.Clock()
return NewClock(h, m) == c
}
// String returns a string representation of the format '15:04'.
func (c Clock) String() string {
return fmt.Sprintf("%02d:%02d", c.Hour(), c.Minute())
}
// NewClock returns a Clock value equal to the provided 24-hour value and minute.
func NewClock(hour, minute int) Clock {
return Clock(time.Duration(hour)*time.Hour + time.Duration(minute)*time.Minute)
}
// NewClockFromTime will return the Clock value of the provided time.
func NewClockFromTime(t time.Time) Clock {
h, m, _ := t.Clock()
return NewClock(h, m)
}
// Minute returns the minute of the Clock value.
func (c Clock) Minute() int {
r := time.Duration(c) % time.Hour
return int(r / time.Minute)
}
// Hour returns the hour of the Clock value.
func (c Clock) Hour() int {
return int(time.Duration(c) / time.Hour)
}
// Format will format the clock value using the same format string
// used by time.Time.
func (c Clock) Format(layout string) string {
return time.Date(0, 0, 0, c.Hour(), c.Minute(), 0, 0, time.UTC).Format(layout)
}
// Value implements the driver.Valuer interface.
func (c Clock) Value() (driver.Value, error) {
return c.String(), nil
}
// Scan implements the sql.Scanner interface.
func (c *Clock) Scan(value interface{}) error {
var parsed Clock
var err error
switch t := value.(type) {
case []byte:
parsed, err = ParseClock(string(t))
case string:
parsed, err = ParseClock(t)
case time.Time:
parsed = NewClock(
t.Hour(),
t.Minute(),
)
default:
return errors.Errorf("could not scan unknown type %T as Clock", t)
}
if err != nil {
return err
}
*c = parsed
return nil
}