-
Notifications
You must be signed in to change notification settings - Fork 0
/
time.go
363 lines (316 loc) · 8.97 KB
/
time.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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
package datetime
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
const (
minutesInDay = 60 * 24
secondsInDay = 86400
timeLayout = "15:04"
)
// EmptyTime is a not initialized Time.
var EmptyTime = Time{}
// Time is a data structure to store hours and minutes.
type Time struct {
time.Time
isSet bool
}
// NewTime returns new time from hour and minute.
func NewTime(hour, minute int) Time {
return Time{time.Date(0, 0, 0, hour, minute, 0, 0, time.UTC), true}
}
// NewTimeFromString returns new time from HH:MM string.
func NewTimeFromString(s string) (Time, error) {
d, err := time.Parse(timeLayout, s)
if err != nil {
return Time{}, err
}
return NewFromTime(d), nil
}
// NewFromTime returns new Time from time.Time.
func NewFromTime(t time.Time) Time {
return Time{time.Date(0, 0, 0, t.Hour(), t.Minute(), 0, 0, time.UTC), true}
}
// NowTime returns current time.
func NowTime(tz *time.Location) Time {
now := time.Now().In(tz)
return NewFromTime(now)
}
// ParseTime tries to parse time (HH:MM) using separators: [" ", ":", "-", "_", ",", "."].
func ParseTime(s string) (Time, error) {
if s == "" {
return Time{}, errors.New("time is empty")
}
seps := []string{" ", ":", "-", "_", ",", "."}
for _, sep := range seps {
splitted := strings.Split(s, sep)
if len(splitted) != 2 {
if len(s) != 4 {
continue
}
splitted = []string{string(s[0:2]), string(s[2:4])}
}
splitted[0] = prepareNumber(splitted[0], false)
hour, err := strconv.Atoi(splitted[0])
if err != nil {
return Time{}, fmt.Errorf("parse hour=%s: %w", splitted[0], err)
}
if hour < 0 || hour > 23 {
return Time{}, fmt.Errorf("invalid hour=%d", hour)
}
splitted[1] = prepareNumber(splitted[1], false)
minute, err := strconv.Atoi(splitted[1])
if err != nil {
return Time{}, fmt.Errorf("parse minute=%s: %w", splitted[1], err)
}
if minute < 0 || minute > 59 {
return Time{}, fmt.Errorf("invalid minute=%d", minute)
}
return NewTime(hour, minute), nil
}
return Time{}, fmt.Errorf("invalid time=%s", s)
}
// String returns time in HH:MM format.
func (t Time) String() string {
return t.Format(timeLayout)
}
// Range substracts low from high time and returns duration between it.
func (low Time) Range(high Time) time.Duration {
return time.Hour*time.Duration(high.Hour()-low.Hour()) +
time.Minute*time.Duration(high.Minute()-low.Minute())
}
// RangeUp returns duration from low time to high time ignoring dates.
func (low Time) RangeUp(high Time) time.Duration {
var hours, minutes int
if high.Hour() < low.Hour() {
hours = 24 - low.Hour() + high.Hour()
} else {
hours = high.Hour() - low.Hour()
}
if high.Minute() < low.Minute() {
hours -= 1
if hours < 0 {
hours = 23
}
minutes = 60 - low.Minute() + high.Minute()
} else {
minutes = high.Minute() - low.Minute()
}
return time.Hour*time.Duration(hours) + time.Minute*time.Duration(minutes)
}
// AddTime adds howMuch to time.
func (t Time) AddTime(howMuch time.Duration) Time {
minutes := int(howMuch.Minutes())
for minutes > minutesInDay {
minutes -= minutesInDay
}
tillEnd := t.MinutesTillDayEnd(EmptyTime)
if minutes < tillEnd {
return t.addTime(time.Minute, minutes)
}
afterDayStart := minutes - tillEnd
return NewTime(0, 0).addTime(time.Minute, afterDayStart)
}
func (t Time) addTime(what time.Duration, howMuch int) Time {
return Time{t.Add(what * time.Duration(howMuch)), true}
}
// SubTime substracts howMuch from time.
func (t Time) SubTime(howMuch time.Duration) Time {
minutes := int(howMuch.Minutes())
for minutes > minutesInDay {
minutes -= minutesInDay
}
fromBegin := t.MinutesFromDayBegin(EmptyTime)
if minutes < fromBegin {
return t.subTime(time.Minute, minutes)
}
beforeDayStart := minutes - fromBegin - 1
return NewTime(23, 59).subTime(time.Minute, beforeDayStart)
}
func (t Time) subTime(what time.Duration, howMuch int) Time {
return t.addTime(what, -1*howMuch)
}
// MinutesFromDayBegin returns number of minutes passed from the beginning of the day.
func (t Time) MinutesFromDayBegin(dayStartTime Time) int {
var hours int
if t.Hour() < dayStartTime.Hour() {
hours = 24 - dayStartTime.Hour() - t.Hour()
} else {
hours = t.Hour() - dayStartTime.Hour()
}
return hours*60 + t.Minute()
}
// MinutesTillDayEnd returns number of minutes remaining to the end of the day.
func (t Time) MinutesTillDayEnd(dayStartTime Time) int {
return minutesInDay - t.MinutesFromDayBegin(dayStartTime)
}
// EqualTime returns true if times are equal.
func (t Time) EqualTime(other Time) bool {
return t.Hour() == other.Hour() && t.Minute() == other.Minute()
}
// IsBefore returns true if reciever is before or equal to argument.
func (t Time) IsBefore(other Time) bool {
if t.Hour() > other.Hour() {
return false
}
if t.Hour() == other.Hour() && t.Minute() > other.Minute() {
return false
}
return true
}
// IsArgBefore returns true if argument is before or equal to reciever.
func (t Time) IsArgBefore(other Time) bool {
if t.Hour() < other.Hour() {
return false
}
if t.Hour() == other.Hour() && t.Minute() < other.Minute() {
return false
}
return true
}
// IsBefore returns true if reciever is STRICTLY before argument.
func (t Time) IsBeforeStrict(other Time) bool {
if t.Hour() > other.Hour() {
return false
}
if t.Hour() == other.Hour() && t.Minute() >= other.Minute() {
return false
}
return true
}
// IsArgBefore returns true if argument is STRICTLY before reciever.
func (t Time) IsArgBeforeStrict(other Time) bool {
if t.Hour() < other.Hour() {
return false
}
if t.Hour() == other.Hour() && t.Minute() <= other.Minute() {
return false
}
return true
}
// IsAfter returns true if reciever is after or equal to argument.
func (t Time) IsAfter(other Time) bool {
return t.IsArgBefore(other)
}
// IsArgAfter returns true if argument is after or equal to reciever.
func (t Time) IsArgAfter(other Time) bool {
return t.IsBefore(other)
}
// IsAfterStrict returns true if reciever is STRICTLY after argument.
func (t Time) IsAfterStrict(other Time) bool {
return t.IsArgBeforeStrict(other)
}
// IsArgAfterStrict returns true if argument is STRICTLY after reciever.
func (t Time) IsArgAfterStrict(other Time) bool {
return t.IsBeforeStrict(other)
}
// SmartDiff returns diff where reciever is start and argument is end
func (start Time) SmartDiff(end Time) time.Duration {
var (
startMinutes = start.MinutesFromDayBegin(EmptyTime)
endMinutes = end.MinutesFromDayBegin(EmptyTime)
)
if endMinutes >= startMinutes {
return time.Minute * time.Duration(endMinutes-startMinutes)
}
return time.Minute * time.Duration(endMinutes+start.MinutesTillDayEnd(EmptyTime))
}
// RoundDownToFives returns time rounded to nearest 5 minutes
func (t Time) RoundDownToFives() Time {
m := t.Minute()
for i := 1; i <= 6; i++ {
base := i * 10
if m < base {
if m <= base-5 {
m = base - 10
} else {
m = base - 5
}
break
}
}
return NewTime(t.Hour(), m)
}
// RoundUpToFives adds 5 minutes and then RoundToFives
func (t Time) RoundUpToFives() Time {
return NewFromTime(t.RoundDownToFives().Add(5 * time.Minute))
}
// IsZero returns true if time is empty.
func (t Time) IsZero() bool {
if t.Time.IsZero() {
return !t.isSet
}
return false
}
// MarshalJSON implements json.Marshaler interface to marshal Time to JSON.
func (t Time) MarshalJSON() ([]byte, error) {
if !t.isSet {
return []byte("null"), nil
}
return json.Marshal(t.String())
}
// UnmarshalJSON implements json.Unmarshaler interface to unmarshal Time from JSON.
func (i *Time) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
res, err := ParseTime(s)
if err != nil {
return err
}
i.Time = res.Time
i.isSet = true
return nil
}
func prepareNumber(s string, isDecimal bool) string {
for i := range s {
if s[i] >= '0' && s[i] <= '9' {
continue
}
if isDecimal && s[i] == '.' {
continue
}
return s[:i]
}
return s
}
// SortingPriority is a mark to group time for sorting.
type SortingPriority int
const (
// LongAgoPriority shoud be first in the list because it should be handled a long time ago.
LongAgoPriority SortingPriority = iota + 1
// BeforePriority should be second in the list because it should be handled not long ago.
BeforePriority
// AfterPriority should be third in the list because it should be handled soon.
AfterPriority
// NotSoonPriority should be last in the list because it should be handled not soon.
NotSoonPriority
)
// GetTimeSortingPriority returns time priority for sorting.
// toCheck is time to check, now is current time, dayStart is day start time (00:00 in general, but not always).
func GetTimeSortingPriority(toCheck, now, dayStart Time) SortingPriority {
if dayStart.IsArgBefore(now) {
if toCheck.IsBefore(dayStart) {
if toCheck.IsBefore(now) {
return BeforePriority
}
return AfterPriority
}
return LongAgoPriority
}
if toCheck.IsAfter(dayStart) {
if toCheck.IsBefore(now) {
return BeforePriority
}
return AfterPriority
}
return NotSoonPriority
}