about summary refs log tree commit diff
path: root/Functions/Calendar/calendar_parse
blob: fabaf7413e9694ebe793319dd77311656c9a1d4e (plain) (blame)
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