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 "fmt"
5 "sort"
6 "strings"
7 "time"
8)
9
10// MergeWindows returns a copy sorted Sunday-first then by start, with
11// windows on the same day that overlap or touch joined into one.
12//
13// Touching counts: "9-12, 12-1" is one stretch of availability, and
14// leaving it as two would make the slot arithmetic offer a 12:00 slot
15// twice, or a 60-minute slot at 11:30 not at all, depending on which
16// window it happened to be looking through. The input is not changed.
17func MergeWindows(ws []Window) []Window {
18 if len(ws) == 0 {
19 return nil
20 }
21 sorted := make([]Window, len(ws))
22 copy(sorted, ws)
23 sort.SliceStable(sorted, func(i, j int) bool {
24 if sorted[i].Weekday != sorted[j].Weekday {
25 return sorted[i].Weekday < sorted[j].Weekday
26 }
27 return sorted[i].StartMin < sorted[j].StartMin
28 })
29 out := []Window{sorted[0]}
30 for _, next := range sorted[1:] {
31 cur := &out[len(out)-1]
32 if next.Weekday == cur.Weekday && next.StartMin <= cur.EndMin {
33 if next.EndMin > cur.EndMin {
34 cur.EndMin = next.EndMin
35 }
36 continue
37 }
38 out = append(out, next)
39 }
40 return out
41}
42
43// DescribeWindows writes the windows out in words, one line per day
44// that has any, Sunday first: "Monday 12:00–15:00, 17:00–19:00".
45//
46// This is what the pane prints, and it is the CONTRACT of the parser
47// in the same way the quick-add pane is: whatever the sentence was
48// read to mean is shown here, in full, before it can be saved.
49func DescribeWindows(ws []Window) []string {
50 byDay := map[time.Weekday][]string{}
51 for _, w := range MergeWindows(ws) {
52 byDay[w.Weekday] = append(byDay[w.Weekday],
53 fmt.Sprintf("%s–%s", clockText(w.StartMin), clockText(w.EndMin)))
54 }
55 var lines []string
56 for d := time.Sunday; d <= time.Saturday; d++ {
57 if spans := byDay[d]; len(spans) > 0 {
58 lines = append(lines, d.String()+" "+strings.Join(spans, ", "))
59 }
60 }
61 return lines
62}
63
64// clockText prints minutes from midnight as HH:MM. 1440 is "24:00":
65// the end of the day, said the way the host typed it, never wrapped to
66// a "00:00" that reads as the start of the same day.
67func clockText(minutes int) string {
68 return fmt.Sprintf("%02d:%02d", minutes/60, minutes%60)
69}
70
71// BookingPaneView is the availability pane: the windows in words and
72// every problem, already formatted so the template does no work.
73type BookingPaneView struct {
74 Lines []string
75 Problems []string
76 Empty bool
77}
78
79// bookingPaneFor renders the sentence for the pane. It is what the
80// live fetch and the scriptless 422 both show, so there is exactly one
81// description of what a sentence was read to mean.
82func bookingPaneFor(text string) BookingPaneView {
83 if strings.TrimSpace(text) == "" {
84 return BookingPaneView{Empty: true}
85 }
86 windows, problems := ParseAvailability(text)
87 return BookingPaneView{Lines: DescribeWindows(windows), Problems: problems}
88}
89