1// This file contains time-related utilities.
2
3package timex
4
5import (
6	"fmt"
7	"time"
8)
9
10// Diff calculates the absolute difference between 2 time instances in
11// years, months, days, hours, minutes and seconds.
12//
13// For details, see https://stackoverflow.com/a/36531443/1705598
14func Diff(a, b time.Time) (year, month, day, hour, min, sec int) {
15	if a.Location() != b.Location() {
16		b = b.In(a.Location())
17	}
18	if a.After(b) {
19		a, b = b, a
20	}
21	y1, M1, d1 := a.Date()
22	y2, M2, d2 := b.Date()
23
24	h1, m1, s1 := a.Clock()
25	h2, m2, s2 := b.Clock()
26
27	year = int(y2 - y1)
28	month = int(M2 - M1)
29	day = int(d2 - d1)
30	hour = int(h2 - h1)
31	min = int(m2 - m1)
32	sec = int(s2 - s1)
33
34	// Normalize negative values
35	if sec < 0 {
36		sec += 60
37		min--
38	}
39	if min < 0 {
40		min += 60
41		hour--
42	}
43	if hour < 0 {
44		hour += 24
45		day--
46	}
47	if day < 0 {
48		// days in month:
49		t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
50		day += 32 - t.Day()
51		month--
52	}
53	if month < 0 {
54		month += 12
55		year--
56	}
57
58	return
59}
60
61// WeekStart returns the time instant pointing to the start of the week given
62// by its year and ISO Week. Weeks are interpreted starting on Monday,
63// so the returned instant will be 00:00 of Monday of the designated week.
64//
65// One nice property of this function is that it handles out-of-range weeks nicely.
66// That is, if you pass 0 for the week, it will be interpreted as the last week
67// of the previous year. If you pass -1 for the week, it will designate
68// the second to last week of the previous year. Similarly, if you pass max week
69// of the year plus 1, it will be interpreted as the first week of the next year etc.
70//
71// This function only returns the given week's first day (Monday), because the
72// last day of the week is always its first day + 6 days.
73//
74// For details, see https://stackoverflow.com/a/52303730/1705598
75func WeekStart(year, week int) time.Time {
76	// Start from the middle of the year:
77	t := time.Date(year, 7, 1, 0, 0, 0, 0, time.UTC)
78
79	// Roll back to Monday:
80	if wd := t.Weekday(); wd == time.Sunday {
81		t = t.AddDate(0, 0, -6)
82	} else {
83		t = t.AddDate(0, 0, -int(wd)+1)
84	}
85
86	// Difference in weeks:
87	_, w := t.ISOWeek()
88	t = t.AddDate(0, 0, (week-w)*7)
89
90	return t
91}
92
93var months = map[string]time.Month{}
94
95func init() {
96	for i := time.January; i <= time.December; i++ {
97		name := i.String()
98		months[name] = i
99		months[name[:3]] = i
100	}
101}
102
103// ParseMonth parses a month given by its name.
104// Both long names such as "January", "February" and short names such as
105// "Jan", "Feb" are recognized.
106//
107// For details, see https://stackoverflow.com/a/59681275/1705598
108func ParseMonth(s string) (time.Month, error) {
109	if m, ok := months[s]; ok {
110		return m, nil
111	}
112
113	return time.January, fmt.Errorf("invalid month '%s'", s)
114}
115
116var weekdays = map[string]time.Weekday{}
117
118func init() {
119	for d := time.Sunday; d <= time.Saturday; d++ {
120		name := d.String()
121		weekdays[name] = d
122		weekdays[name[:3]] = d
123	}
124}
125
126// ParseWeekday parses a weekday given by its name.
127// Both long names such as "Monday", "Tuesday" and short names such as
128// "Mon", "Tue" are recognized.
129//
130// For details, see https://stackoverflow.com/a/52456320/1705598
131func ParseWeekday(s string) (time.Weekday, error) {
132	if d, ok := weekdays[s]; ok {
133		return d, nil
134	}
135
136	return time.Sunday, fmt.Errorf("invalid weekday '%s'", s)
137}
138