oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "regexp"
5 "strconv"
6 "strings"
7 "time"
8)
9
10// clockTime is a wall-clock hour and minute, with set distinguishing
11// "midnight" from "not given".
12type clockTime struct {
13 h, m int
14 set bool
15}
16
17var (
18 // A range: "3-4pm", "3pm to 4:30", "from 9 until 10:15".
19 reRange = regexp.MustCompile(`(?i)\b(?:from\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:-|–|—|to|until|till)\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b`)
20 // A time with minutes: "15:00", "at 3:30pm".
21 reHHMM = regexp.MustCompile(`(?i)\b(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)?\b`)
22 // A time with a meridiem: "3pm", "at 11 am".
23 reMerid = regexp.MustCompile(`(?i)\b(?:at\s+)?(\d{1,2})\s*(am|pm)\b`)
24 // A bare hour, and ONLY after "at": "at 3". Without the "at" this
25 // would eat the 4 in "room 4".
26 reBareAt = regexp.MustCompile(`(?i)\bat\s+(\d{1,2})\b`)
27)
28
29// takeTimes reads a start and possibly an end out of the sentence.
30func takeTimes(c *consumed) (start, end clockTime, found bool) {
31 if m := c.take(reRange); m != nil {
32 s := clock(m[1], m[2])
33 e := clock(m[4], m[5])
34 sm, em := strings.ToLower(m[3]), strings.ToLower(m[6])
35 // "3-4pm" means both are pm: a meridiem on either end applies
36 // to the whole range unless the other says otherwise.
37 if sm == "" {
38 sm = em
39 }
40 if em == "" {
41 em = sm
42 }
43 s = applyMeridiem(s, sm)
44 e = applyMeridiem(e, em)
45 return s, e, true
46 }
47 if m := c.take(reHHMM); m != nil {
48 return applyMeridiem(clock(m[1], m[2]), strings.ToLower(m[3])), clockTime{}, true
49 }
50 if m := c.take(reMerid); m != nil {
51 return applyMeridiem(clock(m[1], ""), strings.ToLower(m[2])), clockTime{}, true
52 }
53 if m := c.take(reBareAt); m != nil {
54 return applyMeridiem(clock(m[1], ""), ""), clockTime{}, true
55 }
56 return clockTime{}, clockTime{}, false
57}
58
59func clock(hh, mm string) clockTime {
60 h, _ := strconv.Atoi(hh)
61 m := 0
62 if mm != "" {
63 m, _ = strconv.Atoi(mm)
64 }
65 return clockTime{h: h, m: m, set: true}
66}
67
68// applyMeridiem resolves am/pm, and guesses when there is none.
69//
70// The guess is the working day: a bare 1 to 7 means the afternoon,
71// because "at 3" is three in the afternoon to everybody who is not a
72// baker. 8 to 12 are taken as written. This is a guess and the pane
73// prints the resolved time, so a wrong one is visible before it is
74// saved rather than after.
75func applyMeridiem(t clockTime, meridiem string) clockTime {
76 if !t.set {
77 return t
78 }
79 switch meridiem {
80 case "pm":
81 if t.h < 12 {
82 t.h += 12
83 }
84 case "am":
85 if t.h == 12 {
86 t.h = 0
87 }
88 default:
89 if t.h >= 1 && t.h <= 7 {
90 t.h += 12
91 }
92 }
93 if t.h > 23 {
94 t.h = 23
95 }
96 return t
97}
98
99var (
100 reDurNum = regexp.MustCompile(`(?i)\bfor\s+(\d+)\s*(minutes?|mins?|hours?|hrs?|h|m)\b`)
101 reDurWord = regexp.MustCompile(`(?i)\bfor\s+(half\s+an?|an?)\s+(hour|hours)\b`)
102)
103
104// takeDuration reads "for 90 minutes", "for 2h", "for an hour".
105func takeDuration(c *consumed) (time.Duration, bool) {
106 if m := c.take(reDurNum); m != nil {
107 n, _ := strconv.Atoi(m[1])
108 unit := strings.ToLower(m[2])
109 if strings.HasPrefix(unit, "h") {
110 return time.Duration(n) * time.Hour, true
111 }
112 return time.Duration(n) * time.Minute, true
113 }
114 if m := c.take(reDurWord); m != nil {
115 if strings.HasPrefix(strings.ToLower(m[1]), "half") {
116 return 30 * time.Minute, true
117 }
118 return time.Hour, true
119 }
120 return 0, false
121}
122
123// weekdayAlt and monthAlt are shared with quickadd_range.go. One
124// spelling of "the names of the days", so a range and a single date can
125// never disagree about whether "tues" is a word.
126const (
127 weekdayAlt = `monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|tues|wed|weds|thu|thur|thurs|fri|sat|sun`
128 monthAlt = `january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|sept|oct|nov|dec`
129)
130
131var (
132 reToday = regexp.MustCompile(`(?i)\b(today|tonight)\b`)
133 reTomorrow = regexp.MustCompile(`(?i)\btomorrow\b`)
134 reWeekday = regexp.MustCompile(`(?i)\b(?:(next|this)\s+)?(?:on\s+)?(` + weekdayAlt + `)\b`)
135 reDayMonth = regexp.MustCompile(`(?i)\b(?:on\s+)?(\d{1,2})(?:st|nd|rd|th)?\s+(` + monthAlt + `)\b`)
136 reMonthDay = regexp.MustCompile(`(?i)\b(?:on\s+)?(` + monthAlt + `)\s+(\d{1,2})(?:st|nd|rd|th)?\b`)
137 reNumeric = regexp.MustCompile(`\b(?:on\s+)?(\d{1,2})/(\d{1,2})(?:/(\d{2,4}))?\b`)
138)
139
140var weekdays = map[string]time.Weekday{
141 "monday": time.Monday, "mon": time.Monday,
142 "tuesday": time.Tuesday, "tue": time.Tuesday, "tues": time.Tuesday,
143 "wednesday": time.Wednesday, "wed": time.Wednesday, "weds": time.Wednesday,
144 "thursday": time.Thursday, "thu": time.Thursday, "thur": time.Thursday, "thurs": time.Thursday,
145 "friday": time.Friday, "fri": time.Friday,
146 "saturday": time.Saturday, "sat": time.Saturday,
147 "sunday": time.Sunday, "sun": time.Sunday,
148}
149
150var months = map[string]time.Month{
151 "january": time.January, "jan": time.January,
152 "february": time.February, "feb": time.February,
153 "march": time.March, "mar": time.March,
154 "april": time.April, "apr": time.April,
155 "may": time.May,
156 "june": time.June, "jun": time.June,
157 "july": time.July, "jul": time.July,
158 "august": time.August, "aug": time.August,
159 "september": time.September, "sep": time.September, "sept": time.September,
160 "october": time.October, "oct": time.October,
161 "november": time.November, "nov": time.November,
162 "december": time.December, "dec": time.December,
163}
164
165// takeDate reads the day the event is on, relative to now.
166//
167// Ordered most specific first. The numeric form is last because
168// "12/3" would otherwise be read out of a time range.
169func takeDate(c *consumed, now time.Time) (time.Time, bool) {
170 if c.take(reToday) != nil {
171 return now, true
172 }
173 if c.take(reTomorrow) != nil {
174 return now.AddDate(0, 0, 1), true
175 }
176 if m := c.take(reDayMonth); m != nil {
177 d, _ := strconv.Atoi(m[1])
178 return onOrAfter(now, months[strings.ToLower(m[2])], d), true
179 }
180 if m := c.take(reMonthDay); m != nil {
181 d, _ := strconv.Atoi(m[2])
182 return onOrAfter(now, months[strings.ToLower(m[1])], d), true
183 }
184 if m := c.take(reWeekday); m != nil {
185 return nextWeekday(now, weekdays[strings.ToLower(m[2])], strings.EqualFold(m[1], "next")), true
186 }
187 if m := c.take(reNumeric); m != nil {
188 d, _ := strconv.Atoi(m[1])
189 mo, _ := strconv.Atoi(m[2])
190 year := now.Year()
191 if m[3] != "" {
192 year, _ = strconv.Atoi(m[3])
193 if year < 100 {
194 year += 2000
195 }
196 }
197 return time.Date(year, time.Month(mo), d, 0, 0, 0, 0, now.Location()), true
198 }
199 return now, false
200}
201
202// nextWeekday is the coming one. "next Tuesday" said on a Tuesday means
203// the one after this, never today — which is what everybody means and
204// what a plain modulo gets wrong.
205func nextWeekday(now time.Time, want time.Weekday, explicitNext bool) time.Time {
206 delta := (int(want) - int(now.Weekday()) + 7) % 7
207 if delta == 0 {
208 delta = 7
209 }
210 if explicitNext && delta < 7 {
211 delta += 7
212 }
213 return now.AddDate(0, 0, delta)
214}
215
216// onOrAfter picks the next occurrence of a day and month, rolling into
217// next year rather than booking a date that has already gone.
218func onOrAfter(now time.Time, month time.Month, day int) time.Time {
219 t := time.Date(now.Year(), month, day, 0, 0, 0, 0, now.Location())
220 if t.Before(now.AddDate(0, 0, -1)) {
221 t = t.AddDate(1, 0, 0)
222 }
223 return t
224}
225