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
|
# Parse the line passed down in the first argument as a calendar entry.
# Sets the values parsed into the associative array reply, consisting of:
# time The time as an integer (as per EPOCHSECONDS) of the (next) event.
# text1 The text from the line not including the date/time, but
# including any WARN or RPT text. This is useful for rescheduling
# events, since the keywords need to be retained in this case.
# warntime Any warning time (WARN keyword) as an integer, else an empty
# string. This is the time of the warning in units of EPOCHSECONDS,
# not the parsed version of the original number (which was a time
# difference).
# warnstr Any warning time as the original string (e.g. "5 mins"), not
# including the WARN keyword.
# schedrpttime The next scheduled recurrence (which may be cancelled
# or rescheduled).
# rpttime The actual occurrence time: the event may have been rescheduled,
# in which case this is the time of the actual event (for use in
# programming warnings etc.) rather than that of the normal
# recurrence (which is recorded by calendar_add as RECURRENCE).
#
# rptstr Any repeat/recurrence time as the original string.
# text2 The text from the line with the date and other keywords and
# values removed.
#
# Note that here an "integer" is a string of digits, not an internally
# formatted integer.
#
# Return status 1 if parsing failed. reply is set to an empty
# in this case. Note the caller is responsible for
# making reply local.
emulate -L zsh
setopt extendedglob
local vdatefmt="%Y%m%dT%H%M%S"
local REPLY REPLY2 timefmt occurrence skip try_to_recover before after
local -a match mbegin mend
integer now then replaced firstsched schedrpt
# Any text matching "OCCURRENCE <timestamp> <disposition>"
# may occur multiple times. We set occurrences[<timestamp>]=disposition.
local -A occurrences
autoload -Uz calendar_scandate
typeset -gA reply
reply=()
if (( $# != 1 )); then
print "Usage: $0 calendar-entry" >&2
return 2
fi
# This call sets REPLY to the date and time in seconds since the epoch,
# REPLY2 to the line with the date and time removed.
calendar_scandate -as $1 || return 1
reply[time]=$(( REPLY ))
schedrpt=${reply[time]}
reply[text1]=${REPLY2##[[:space:]]#}
reply[text2]=${reply[text1]}
while true; do
case ${reply[text2]} in
# First check for a scheduled repeat time. If we don't find one
# we'll use the normal time.
((#b)(*[[:space:]\#])RECURRENCE[[:space:]]##([^[:space:]]##)([[:space:]]*|))
strftime -rs then $vdatefmt ${match[2]} ||
print "format: $vdatefmt, string ${match[2]}" >&2
schedrpt=$then
reply[text2]="${match[1]}${match[3]##[ ]#}"
;;
# Look for specific warn time.
((#b)(|*[[:space:],])WARN[[:space:]](*))
if calendar_scandate -asm -R $reply[time] $match[2]; then
reply[warntime]=$REPLY
reply[warnstr]=${match[2]%%"$REPLY2"}
# Remove spaces and tabs but not newlines from trailing text,
# else the formatting looks funny.
reply[text2]="${match[1]}${REPLY2##[ ]#}"
else
# Just remove the keyword for further parsing
reply[text2]="${match[1]}${match[2]##[ ]#}"
fi
;;
((#b)(|*[[:space:],])RPT[[:space:]](*))
before=${match[1]}
after=${match[2]}
if [[ $after = CANCELLED(|[[:space:]]*) ]]; then
reply[text2]="$before${match[2]##[ ]#}"
reply[rptstr]=CANCELLED
reply[rpttime]=CANCELLED
reply[schedrpttime]=CANCELLED
elif calendar_scandate -a -R $schedrpt $after; then
# It's possible to calculate a recurrence, however we don't
# do that yet. For now just keep the current time as
# the recurrence. Hence we ignore REPLY.
reply[text2]="$before${REPLY2##[ ]#}"
reply[rptstr]=${after%%"$REPLY2"}
# Until we find an individual occurrence, the actual time
# of the event is the regular one.
reply[rpttime]=$schedrpt
else
# Just remove the keyword for further parsing
reply[text2]="$before${after##[[:space:]]#}"
fi
;;
((#b)(|*[[:space:]\#])OCCURRENCE[[:space:]]##([^[:space:]]##)[[:space:]]##([^[:space:]]##)(*))
occurrences[${match[2]}]="${match[3]}"
# as above
reply[text2]="${match[1]}${match[4]##[ ]#}"
;;
(*)
break
;;
esac
done
if [[ -n ${reply[rpttime]} && ${reply[rptstr]} != CANCELLED ]]; then
# Recurring event. We need to find out when it recurs.
(( now = EPOCHSECONDS ))
# First find the next recurrence.
replaced=0
reply[schedrpttime]=$schedrpt
if (( schedrpt >= now )); then
firstsched=$schedrpt
fi
while (( ${reply[schedrpttime]} < now || replaced )); do
if ! calendar_scandate -a -R ${reply[schedrpttime]} ${reply[rptstr]}; then
break
fi
if (( REPLY <= ${reply[schedrpttime]} )); then
# going backwards --- pathological case
break;
fi
reply[schedrpttime]=$REPLY
reply[rpttime]=$REPLY
if (( ${reply[schedrpttime]} > now && firstsched == 0 )); then
firstsched=$REPLY
fi
replaced=0
# do we have an occurrence to compare against?
if (( ${#occurrences} )); then
strftime -s timefmt $vdatefmt ${reply[schedrpttime]}
occurrence=$occurrences[$timefmt]
if [[ -n $occurrence ]]; then
# Yes, this replaces the scheduled one.
replaced=1
fi
fi
done
# Now look through occurrences (values only) and see which are (i) still
# to happen (ii) early than the current rpttime.
for occurrence in $occurrences; do
if [[ $occurrence != CANCELLED ]]; then
strftime -rs then $vdatefmt $occurrence ||
print "format: $vdatefmt, string $occurrence" >&2
if (( then > now && then < ${reply[rpttime]} )); then
reply[rpttime]=$then
fi
fi
done
# Finally, update the scheduled repeat time to the earliest
# possible value. This is so that if an occurrence replacement is
# cancelled we pick up the regular one. Can this happen? Dunno.
reply[schedrpttime]=$firstsched
fi
reply[text2]="${reply[text2]##[[:space:],]#}"
return 0
|