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 "strings"
5 "time"
6)
7
8// The low-level readers: one content line into name, parameters and
9// value, and the handful of value formats iCalendar uses. Split from
10// ics_import.go so neither file passes the 300-line cap, and because
11// these are the pieces with no knowledge of what a VEVENT is.
12
13// splitLine breaks "DTSTART;TZID=Europe/Dublin:20260902T090000" into
14// its name, parameters and value.
15func splitLine(s string) (name, params, value string) {
16 colon := strings.Index(s, ":")
17 if colon < 0 {
18 return strings.ToUpper(strings.TrimSpace(s)), "", ""
19 }
20 head, value := s[:colon], s[colon+1:]
21 if semi := strings.Index(head, ";"); semi >= 0 {
22 return strings.ToUpper(head[:semi]), head[semi+1:], value
23 }
24 return strings.ToUpper(head), "", value
25}
26
27func tzidOf(params string) string {
28 for _, p := range strings.Split(params, ";") {
29 if k, v, ok := strings.Cut(p, "="); ok && strings.EqualFold(k, "TZID") {
30 return strings.Trim(strings.TrimSpace(v), `"`)
31 }
32 }
33 return ""
34}
35
36// parseICSTime reads the three forms a DTSTART can take, and reports
37// whether it was a date.
38func parseICSTime(v, params string) (local string, allDay bool, ok bool) {
39 v = strings.TrimSpace(v)
40 if strings.Contains(strings.ToUpper(params), "VALUE=DATE") || len(v) == 8 {
41 if t, err := time.Parse("20060102", v); err == nil {
42 return t.Format(LayoutDate), true, true
43 }
44 }
45 // A trailing Z is UTC. The wall time is kept as written and the
46 // zone recorded separately, so the record stays "what was said".
47 if strings.HasSuffix(v, "Z") {
48 if t, err := time.Parse("20060102T150405Z", v); err == nil {
49 return t.Format(LayoutLocal), false, true
50 }
51 }
52 if t, err := time.Parse("20060102T150405", v); err == nil {
53 return t.Format(LayoutLocal), false, true
54 }
55 return "", false, false
56}
57
58// parseDurationMinutes reads the subset of ISO 8601 durations a
59// calendar actually emits.
60func parseDurationMinutes(v string) int {
61 v = strings.TrimSpace(strings.ToUpper(v))
62 v = strings.TrimPrefix(v, "P")
63 var total, n int
64 timePart := false
65 for _, c := range v {
66 switch {
67 case c >= '0' && c <= '9':
68 n = n*10 + int(c-'0')
69 case c == 'T':
70 timePart = true
71 n = 0
72 case c == 'W':
73 total += n * 7 * 24 * 60
74 n = 0
75 case c == 'D':
76 total += n * 24 * 60
77 n = 0
78 case c == 'H':
79 total += n * 60
80 n = 0
81 case c == 'M' && timePart:
82 total += n
83 n = 0
84 default:
85 n = 0
86 }
87 }
88 return total
89}
90
91// unescapeText reverses the RFC's TEXT escaping. Backslash LAST, the
92// mirror of escapeText's first — otherwise "\\n" (an escaped
93// backslash followed by an n) becomes a newline.
94func unescapeText(s string) string {
95 s = strings.ReplaceAll(s, `\n`, "\n")
96 s = strings.ReplaceAll(s, `\N`, "\n")
97 s = strings.ReplaceAll(s, `\,`, ",")
98 s = strings.ReplaceAll(s, `\;`, ";")
99 s = strings.ReplaceAll(s, `\\`, `\`)
100 return s
101}
102