-
Notifications
You must be signed in to change notification settings - Fork 8
/
week.go
278 lines (215 loc) · 6.24 KB
/
week.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
// Package week provides a simple data type representing a week date as defined by ISO 8601.
package week
import (
"database/sql/driver"
"fmt"
"time"
"github.com/pkg/errors"
)
// Week represents a week date as defined by ISO 8601. Week can be marshaled to and unmarshaled from
// numerous formats such as plain text or json.
type Week struct {
year int
week int
}
// New creates a new Week object from the specified year and week.
func New(year, week int) (Week, error) {
err := checkYearAndWeek(year, week)
if err != nil {
return Week{}, err
}
return Week{year: year, week: week}, nil
}
// Year returns the year of the ISO week date.
func (w *Week) Year() int {
return w.year
}
// Week returns the week of the ISO week date.
func (w *Week) Week() int {
return w.week
}
// Next calculates and returns the next week. If the next week is invalid (year > 9999) the function
// returns an error.
func (w *Week) Next() (Week, error) {
return w.Add(1)
}
// Previous calculates and returns the previous week. If the previous week is invalid (year < 0) the
// function returns an error.
func (w *Week) Previous() (Week, error) {
return w.Add(-1)
}
// Add calculates and returns a week that is the given positive distance (number of weeks) from the current week
func (w *Week) Add(weeks int) (Week, error) {
sign := 1
if weeks < 0 {
sign = -1
}
year := w.year
week := w.week + weeks
maxWeeks := weeksInYear(w.year)
for {
if week <= maxWeeks && week >= 0 {
break
}
year += sign
if sign == 1 {
week -= maxWeeks
}
maxWeeks = weeksInYear(year)
if sign == -1 {
week += maxWeeks
}
}
if week == 0 {
year += sign
week = weeksInYear(year)
}
return New(year, week)
}
// Sub calculates the positive difference between w and u (w-u) in number of weeks
func (w *Week) Sub(u Week) int {
direction := 1
smaller := u
bigger := *w
if smaller.year > bigger.year {
direction = -1
smaller, bigger = bigger, smaller
}
weeks := 0
for year := smaller.year; year < bigger.year; year++ {
weeks += weeksInYear(year)
}
weeks += bigger.week - smaller.week
return weeks * direction
}
// After reports whether the week instant w is after u
func (w *Week) After(u Week) bool {
return w.Sub(u) > 0
}
// Before reports whether the week w is before u
func (w *Week) Before(u Week) bool {
return w.Sub(u) < 0
}
// Equal reports whether w and u are the same week of the same year
func (w *Week) Equal(u Week) bool {
return *w == u
}
// UnmarshalJSON implements json.Unmarshaler for Week.
func (w *Week) UnmarshalJSON(data []byte) error {
if data[0] != '"' || data[len(data)-1] != '"' {
return errors.New("unable to unmarshal json: string literal expected")
}
year, week, err := decodeISOWeekDate(data[1 : len(data)-1])
if err != nil {
return errors.Wrap(err, "unable to unmarshal json")
}
w.year, w.week = year, week
return nil
}
// MarshalJSON implements json.Marshaler for Week.
func (w Week) MarshalJSON() ([]byte, error) {
raw, err := encodeISOWeekDate(w.year, w.week)
if err != nil {
return nil, errors.Wrap(err, "unable to marshal json")
}
json := make([]byte, 0, len(raw)+2)
json = append(json, '"')
json = append(json, raw...)
json = append(json, '"')
return json, nil
}
// UnmarshalText implements TextUnmarshaler for Week.
func (w *Week) UnmarshalText(data []byte) error {
year, week, err := decodeISOWeekDate(data)
if err != nil {
return errors.Wrap(err, "unable to unmarshal text")
}
w.year, w.week = year, week
return nil
}
// MarshalText implements TextMarshaler for Week.
func (w Week) MarshalText() ([]byte, error) {
text, err := encodeISOWeekDate(w.year, w.week)
if err != nil {
return nil, errors.Wrap(err, "unable to marshal text")
}
return text, nil
}
// Value implements Valuer for Week.
func (w Week) Value() (driver.Value, error) {
text, err := encodeISOWeekDate(w.year, w.week)
if err != nil {
return nil, errors.Wrap(err, "unable to create value")
}
return driver.Value(string(text)), nil
}
// Scan implements scanner for Week.
func (w *Week) Scan(src interface{}) error {
var year int
var week int
var err error
switch val := src.(type) {
case string:
year, week, err = decodeISOWeekDate([]byte(val))
case []byte:
year, week, err = decodeISOWeekDate(val)
default:
return errors.New("unable to scan value: incompatible type")
}
if err != nil {
return errors.Wrap(err, "unable to scan value")
}
w.year, w.week = year, week
return nil
}
// FromTime converts time.Time into a Week
func FromTime(t time.Time) Week {
year, week := t.ISOWeek()
return Week{year: year, week: week}
}
// Time converts a week to a time.Time object which represents the midnight of the provided weekday.
func (w *Week) Time(weekday time.Weekday) time.Time {
// The implementation based on the method on the ordinal day of the year and described here:
// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year,_week_number_and_weekday
isoWeekday := convertToISOWeekday(weekday)
jan4th := time.Date(w.Year(), 1, 4, 0, 0, 0, 0, time.UTC)
correction := convertToISOWeekday(jan4th.Weekday()) + 3
ordinal := w.Week()*7 + isoWeekday - correction
year, ordinal := normalizeOrdinal(w.Year(), ordinal)
return time.Date(year, 1, ordinal, 0, 0, 0, 0, time.UTC)
}
// normalizeOrdinal checks if ordinal number is in range between 1 and actual number of days
// in the specified year. If its our of this range, values for the year and ordinal date
// are adjusted
func normalizeOrdinal(year, ordinal int) (normalizedYear, normalizedOrdinal int) {
daysInYear := 365
if ordinal < 1 {
if isLeapYear(year - 1) {
daysInYear = 366
}
return year - 1, daysInYear + ordinal
}
if isLeapYear(year) {
daysInYear = 366
}
if ordinal > daysInYear {
return year + 1, ordinal - daysInYear
}
return year, ordinal
}
// convertToISOWeekday convert time.Weekday value to an ISO representation of weekday which declares
// that the first day of the week is Monday=1 and last is Sunday=7
func convertToISOWeekday(weekday time.Weekday) int {
if weekday == time.Sunday {
return 7
}
return int(weekday)
}
// String converts a Week into its string representation
func (w Week) String() string {
err := checkYearAndWeek(w.year, w.week)
if err != nil {
panic(err)
}
return fmt.Sprintf(weekDateFormat, w.year, w.week)
}