File: | libsynthesis/src/sysync/rrules.cpp |
Warning: | line 2260, column 28 The result of the '<<' expression is undefined |
1 | /* | |||
2 | * File: rrules.cpp | |||
3 | * | |||
4 | * Author: Lukas Zeller (luz@plan44.ch) | |||
5 | * | |||
6 | * Parser/Generator routines for vCalendar RRULES | |||
7 | * | |||
8 | * Copyright (c) 2001-2011 by Synthesis AG + plan44.ch | |||
9 | * | |||
10 | * 2004-11-23 : luz : created from exctracts from vcalendaritemtype.cpp | |||
11 | * | |||
12 | */ | |||
13 | #include "prefix_file.h" | |||
14 | ||||
15 | #include <stdarg.h> | |||
16 | ||||
17 | #include "rrules.h" | |||
18 | ||||
19 | ||||
20 | #if defined(SYSYNC_TOOL) | |||
21 | #include "syncappbase.h" // for CONSOLEPRINTF | |||
22 | #endif | |||
23 | ||||
24 | using namespace sysync; | |||
25 | ||||
26 | namespace sysync { | |||
27 | ||||
28 | ||||
29 | // Names of weekdays in RRULEs | |||
30 | const char* const RRULE_weekdays[ DaysOfWeek ] = { | |||
31 | "SU", | |||
32 | "MO", | |||
33 | "TU", | |||
34 | "WE", | |||
35 | "TH", | |||
36 | "FR", | |||
37 | "SA" | |||
38 | }; | |||
39 | ||||
40 | ||||
41 | // Support for SySync Diagnostic Tool | |||
42 | #ifdef SYSYNC_TOOL | |||
43 | ||||
44 | // convert between RRULE and internal format | |||
45 | int rruleConv(int argc, const char *argv[]) | |||
46 | { | |||
47 | if (argc<0) { | |||
48 | // help requested | |||
49 | CONSOLEPRINTF((" rrule <input mode> <output mode> <input> ..."))SySync_ConsolePrintf(stderr, "SYSYNC " " rrule <input mode> <output mode> <input> ..." "\n"); | |||
50 | CONSOLEPRINTF((" Convert between RRULE representations:"))SySync_ConsolePrintf(stderr, "SYSYNC " " Convert between RRULE representations:" "\n"); | |||
51 | CONSOLEPRINTF((" mode \"i\" : internal format: startdate,freq,freqmod,interval,firstmask,lastmask,until"))SySync_ConsolePrintf(stderr, "SYSYNC " " mode \"i\" : internal format: startdate,freq,freqmod,interval,firstmask,lastmask,until" "\n"); | |||
52 | CONSOLEPRINTF((" (firstmask/lastmask in decimal or hex, until in ISO8601 format)"))SySync_ConsolePrintf(stderr, "SYSYNC " " (firstmask/lastmask in decimal or hex, until in ISO8601 format)" "\n"); | |||
53 | CONSOLEPRINTF((" mode \"t\" : output only, like \"i\" but also shows table of first 10 recurrence dates"))SySync_ConsolePrintf(stderr, "SYSYNC " " mode \"t\" : output only, like \"i\" but also shows table of first 10 recurrence dates" "\n"); | |||
54 | CONSOLEPRINTF((" mode \"1\" : startdate,RRULE according to vCalendar 1.0 specs"))SySync_ConsolePrintf(stderr, "SYSYNC " " mode \"1\" : startdate,RRULE according to vCalendar 1.0 specs" "\n"); | |||
55 | CONSOLEPRINTF((" mode \"2\" : startdate,RRULE according to iCalendar (RFC2445) specs"))SySync_ConsolePrintf(stderr, "SYSYNC " " mode \"2\" : startdate,RRULE according to iCalendar (RFC2445) specs" "\n"); | |||
56 | return EXIT_SUCCESS0; | |||
57 | } | |||
58 | ||||
59 | // check for argument | |||
60 | if (argc!=3) { | |||
61 | CONSOLEPRINTF(("3 arguments required"))SySync_ConsolePrintf(stderr, "SYSYNC " "3 arguments required" "\n"); | |||
62 | return EXIT_FAILURE1; | |||
63 | } | |||
64 | // mode | |||
65 | char inmode,outmode; | |||
66 | // internal representation | |||
67 | lineartime_t start; | |||
68 | char freq; | |||
69 | char freqmod; | |||
70 | sInt16 interval; | |||
71 | fieldinteger_t firstmask; | |||
72 | fieldinteger_t lastmask; | |||
73 | lineartime_t until; | |||
74 | // time zone stuff | |||
75 | timecontext_t rulecontext=TCTX_UNKNOWN((timecontext_t) ((tctx_tz_unknown) | TCTX_SYMBOLIC_TZ)); | |||
76 | timecontext_t untilcontext; | |||
77 | GZones zones; | |||
78 | // RRULE | |||
79 | char isodate[50]; | |||
80 | char rulebuf[200]; | |||
81 | string rrule,iso; | |||
82 | // get input mode | |||
83 | inmode=tolower(*(argv[0])); | |||
84 | if (inmode!='i' && inmode!='1' && inmode!='2') { | |||
85 | CONSOLEPRINTF(("invalid input mode, must be i, 1 or 2"))SySync_ConsolePrintf(stderr, "SYSYNC " "invalid input mode, must be i, 1 or 2" "\n"); | |||
86 | return EXIT_FAILURE1; | |||
87 | } | |||
88 | // get output mode | |||
89 | outmode=tolower(*(argv[1])); | |||
90 | if (outmode!='i' && outmode!='t' && outmode!='1' && outmode!='2') { | |||
91 | CONSOLEPRINTF(("invalid output mode, must be i, t, 1 or 2"))SySync_ConsolePrintf(stderr, "SYSYNC " "invalid output mode, must be i, t, 1 or 2" "\n"); | |||
92 | return EXIT_FAILURE1; | |||
93 | } | |||
94 | CONSOLEPRINTF((""))SySync_ConsolePrintf(stderr, "SYSYNC " "" "\n"); | |||
95 | // reset params | |||
96 | freq='0'; // frequency = none | |||
97 | freqmod=' '; // frequency modifier | |||
98 | interval=0; // interval | |||
99 | firstmask=0; // day mask counted from the first day of the period | |||
100 | lastmask=0; // day mask counted from the last day of the period | |||
101 | until= noLinearTime; // last day | |||
102 | // get input and convert to internal format (if needed) | |||
103 | if (inmode=='i') { | |||
104 | if (sscanf(argv[2], | |||
105 | "%[^,],%c,%c,%hd,%lld,%lld,%s", | |||
106 | rulebuf, | |||
107 | &freq, | |||
108 | &freqmod, | |||
109 | &interval, | |||
110 | &firstmask, | |||
111 | &lastmask, | |||
112 | isodate | |||
113 | )!=7) { | |||
114 | CONSOLEPRINTF(("invalid internal format input"))SySync_ConsolePrintf(stderr, "SYSYNC " "invalid internal format input" "\n"); | |||
115 | return EXIT_FAILURE1; | |||
116 | } | |||
117 | // - convert dates | |||
118 | ISO8601StrToTimestamp(rulebuf,start,rulecontext); // Start determines rule context | |||
119 | ISO8601StrToTimestamp(isodate,until,untilcontext); // until will be converted to same context | |||
120 | TzConvertTimestamp(until,untilcontext,rulecontext,&zones); | |||
121 | } | |||
122 | else if (inmode=='1') { | |||
123 | if (sscanf(argv[2],"%[^,],%[^\n]",isodate,rulebuf)!=2) { | |||
124 | CONSOLEPRINTF(("error, expected: startdate,RRULE"))SySync_ConsolePrintf(stderr, "SYSYNC " "error, expected: startdate,RRULE" "\n"); | |||
125 | return EXIT_FAILURE1; | |||
126 | } | |||
127 | ISO8601StrToTimestamp(isodate,start,rulecontext); | |||
128 | if (!RRULE1toInternal( | |||
129 | rulebuf, // RRULE string to be parsed | |||
130 | start, // reference date for parsing RRULE | |||
131 | rulecontext, // time context of RRULE | |||
132 | freq, | |||
133 | freqmod, | |||
134 | interval, | |||
135 | firstmask, | |||
136 | lastmask, | |||
137 | until, | |||
138 | untilcontext, | |||
139 | NULL__null | |||
140 | )) { | |||
141 | CONSOLEPRINTF(("invalid/unsupported RRULE type 1 specification"))SySync_ConsolePrintf(stderr, "SYSYNC " "invalid/unsupported RRULE type 1 specification" "\n"); | |||
142 | return EXIT_FAILURE1; | |||
143 | } | |||
144 | } | |||
145 | else if (inmode=='2') { | |||
146 | if (sscanf(argv[2],"%[^,],%[^\n]",isodate,rulebuf)!=2) { | |||
147 | CONSOLEPRINTF(("error, expected: startdate,RRULE"))SySync_ConsolePrintf(stderr, "SYSYNC " "error, expected: startdate,RRULE" "\n"); | |||
148 | return EXIT_FAILURE1; | |||
149 | } | |||
150 | ISO8601StrToTimestamp(isodate,start,rulecontext); | |||
151 | if (!RRULE2toInternal( | |||
152 | rulebuf, // RRULE string to be parsed | |||
153 | start, // reference date for parsing RRULE | |||
154 | rulecontext, // time context of RRULE | |||
155 | freq, | |||
156 | freqmod, | |||
157 | interval, | |||
158 | firstmask, | |||
159 | lastmask, | |||
160 | until, | |||
161 | untilcontext, | |||
162 | NULL__null | |||
163 | )) { | |||
164 | CONSOLEPRINTF(("invalid/unsupported RRULE type 2 specification"))SySync_ConsolePrintf(stderr, "SYSYNC " "invalid/unsupported RRULE type 2 specification" "\n"); | |||
165 | return EXIT_FAILURE1; | |||
166 | } | |||
167 | } | |||
168 | // convert to rrule (if needed) and output | |||
169 | TimestampToISO8601Str(iso,start,rulecontext,true); | |||
170 | if (outmode=='i' || outmode=='t') { | |||
171 | TimestampToISO8601Str(rrule,until,TCTX_UTC((timecontext_t) ((tctx_tz_UTC) | TCTX_SYMBOLIC_TZ)),true); | |||
172 | CONSOLEPRINTF((SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
173 | "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s",SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
174 | iso.c_str(),SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
175 | freq,SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
176 | freqmod,SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
177 | interval,SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
178 | firstmask,SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
179 | lastmask,SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
180 | rrule.c_str()SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()) | |||
181 | ))SySync_ConsolePrintf(stderr, "SYSYNC " "Internal format (start,freq,freqmod,interval,firstmask,lastmask,until:\n%s,%c,%c,%hd,0x%llX,0x%llX,%s" "\n", iso.c_str(), freq, freqmod, interval, firstmask, lastmask , rrule.c_str()); | |||
182 | // extra info if we have 't' table mode | |||
183 | lineartime_t occurrence; | |||
184 | if (outmode=='t') { | |||
185 | sInt16 cnt; | |||
186 | for (cnt=1; cnt<=10; cnt++) { | |||
187 | // calculate date of cnt-th recurrence | |||
188 | if (!endDateFromCount( | |||
189 | occurrence, | |||
190 | start, | |||
191 | freq, freqmod, | |||
192 | interval, | |||
193 | firstmask, lastmask, | |||
194 | cnt, true, // counting occurrences | |||
195 | NULL__null | |||
196 | )) | |||
197 | break; | |||
198 | // see if limit already reached | |||
199 | if (occurrence>until) { | |||
200 | cnt=0; | |||
201 | break; | |||
202 | } | |||
203 | // show this recurrence | |||
204 | TimestampToISO8601Str(iso,occurrence,rulecontext,true); | |||
205 | CONSOLEPRINTF((SySync_ConsolePrintf(stderr, "SYSYNC " "%3d. occurrence at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
206 | "%3d. occurrence at %s (%s)",SySync_ConsolePrintf(stderr, "SYSYNC " "%3d. occurrence at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
207 | cnt,SySync_ConsolePrintf(stderr, "SYSYNC " "%3d. occurrence at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
208 | iso.c_str(),SySync_ConsolePrintf(stderr, "SYSYNC " "%3d. occurrence at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
209 | RRULE_weekdays[lineartime2weekday(occurrence)]SySync_ConsolePrintf(stderr, "SYSYNC " "%3d. occurrence at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
210 | ))SySync_ConsolePrintf(stderr, "SYSYNC " "%3d. occurrence at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]); | |||
211 | } | |||
212 | // show end date if not all recurrences shown already | |||
213 | if (cnt>10) { | |||
214 | // calculate occurrence count | |||
215 | if (countFromEndDate( | |||
216 | cnt, true, // counting occurrences | |||
217 | start, | |||
218 | freq, freqmod, | |||
219 | interval, | |||
220 | firstmask, lastmask, | |||
221 | until, | |||
222 | NULL__null | |||
223 | )) { | |||
224 | if (cnt==0) { | |||
225 | CONSOLEPRINTF((SySync_ConsolePrintf(stderr, "SYSYNC " "last occurrence: <none> (repeating infinitely)" "\n", cnt, iso.c_str()) | |||
226 | "last occurrence: <none> (repeating infinitely)",SySync_ConsolePrintf(stderr, "SYSYNC " "last occurrence: <none> (repeating infinitely)" "\n", cnt, iso.c_str()) | |||
227 | cnt,SySync_ConsolePrintf(stderr, "SYSYNC " "last occurrence: <none> (repeating infinitely)" "\n", cnt, iso.c_str()) | |||
228 | iso.c_str()SySync_ConsolePrintf(stderr, "SYSYNC " "last occurrence: <none> (repeating infinitely)" "\n", cnt, iso.c_str()) | |||
229 | ))SySync_ConsolePrintf(stderr, "SYSYNC " "last occurrence: <none> (repeating infinitely)" "\n", cnt, iso.c_str()); | |||
230 | } | |||
231 | else { | |||
232 | // convert back to date | |||
233 | if (endDateFromCount( | |||
234 | occurrence, | |||
235 | start, | |||
236 | freq, freqmod, | |||
237 | interval, | |||
238 | firstmask, lastmask, | |||
239 | cnt, true, // counting occurrences | |||
240 | NULL__null | |||
241 | )) { | |||
242 | TimestampToISO8601Str(iso,occurrence,rulecontext,true); | |||
243 | CONSOLEPRINTF((SySync_ConsolePrintf(stderr, "SYSYNC " "%3d./LAST occ. at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
244 | "%3d./LAST occ. at %s (%s)",SySync_ConsolePrintf(stderr, "SYSYNC " "%3d./LAST occ. at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
245 | cnt,SySync_ConsolePrintf(stderr, "SYSYNC " "%3d./LAST occ. at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
246 | iso.c_str(),SySync_ConsolePrintf(stderr, "SYSYNC " "%3d./LAST occ. at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
247 | RRULE_weekdays[lineartime2weekday(occurrence)]SySync_ConsolePrintf(stderr, "SYSYNC " "%3d./LAST occ. at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]) | |||
248 | ))SySync_ConsolePrintf(stderr, "SYSYNC " "%3d./LAST occ. at %s (%s)" "\n", cnt, iso.c_str(), RRULE_weekdays[lineartime2weekday(occurrence )]); | |||
249 | } | |||
250 | } | |||
251 | } | |||
252 | } | |||
253 | } | |||
254 | } | |||
255 | else if (outmode=='1') { | |||
256 | if (!internalToRRULE1( | |||
257 | rrule, // receives RRULE string | |||
258 | freq, | |||
259 | freqmod, | |||
260 | interval, | |||
261 | firstmask, | |||
262 | lastmask, | |||
263 | until, | |||
264 | rulecontext, | |||
265 | NULL__null | |||
266 | )) { | |||
267 | CONSOLEPRINTF(("Cannot show as RRULE type 1"))SySync_ConsolePrintf(stderr, "SYSYNC " "Cannot show as RRULE type 1" "\n"); | |||
268 | return EXIT_FAILURE1; | |||
269 | } | |||
270 | CONSOLEPRINTF(("RRULE type 1 format (start,rrule):\n%s,%s",iso.c_str(),rrule.c_str()))SySync_ConsolePrintf(stderr, "SYSYNC " "RRULE type 1 format (start,rrule):\n%s,%s" "\n",iso.c_str(),rrule.c_str()); | |||
271 | } | |||
272 | else if (outmode=='2') { | |||
273 | if (!internalToRRULE2( | |||
274 | rrule, // receives RRULE string | |||
275 | freq, | |||
276 | freqmod, | |||
277 | interval, | |||
278 | firstmask, | |||
279 | lastmask, | |||
280 | until, | |||
281 | rulecontext, | |||
282 | NULL__null | |||
283 | )) { | |||
284 | CONSOLEPRINTF(("Cannot show as RRULE type 2"))SySync_ConsolePrintf(stderr, "SYSYNC " "Cannot show as RRULE type 2" "\n"); | |||
285 | return EXIT_FAILURE1; | |||
286 | } | |||
287 | CONSOLEPRINTF(("RRULE type 2 format (start,rrule):\n%s,%s",iso.c_str(),rrule.c_str()))SySync_ConsolePrintf(stderr, "SYSYNC " "RRULE type 2 format (start,rrule):\n%s,%s" "\n",iso.c_str(),rrule.c_str()); | |||
288 | } | |||
289 | return EXIT_SUCCESS0; | |||
290 | } // rruleConv | |||
291 | ||||
292 | #endif // SYSYNC_TOOL | |||
293 | ||||
294 | ||||
295 | ||||
296 | /* | |||
297 | // sample of a field block: | |||
298 | ||||
299 | Offset=0: <field name="RR_FREQ" type="string" compare="conflict"/> | |||
300 | - frequency codes: | |||
301 | 0 = none, | |||
302 | s = secondly, | |||
303 | m = minutely, | |||
304 | h = hourly, | |||
305 | D = daily, | |||
306 | W = weekly, | |||
307 | M = monthly, | |||
308 | Y = yearly | |||
309 | - frequency modifiers | |||
310 | space = none | |||
311 | s = by second list | |||
312 | m = by minute list | |||
313 | h = by hour list | |||
314 | W = by weekday list | |||
315 | D = by monthday list | |||
316 | Y = by yearday list | |||
317 | N = by weeknumber list | |||
318 | M = by monthlist | |||
319 | P = by setposlist | |||
320 | ||||
321 | Offset=1: <field name="RR_INTERVAL" type="integer" compare="conflict"/> | |||
322 | - interval | |||
323 | ||||
324 | Offset=2: <field name="RR_FMASK" type="integer" compare="conflict"/> | |||
325 | - bit-coded list of items according to frequency modifier, | |||
326 | relative to the beginning of the interval | |||
327 | - for seconds, minutes, hours: Bit0=first, Bit1=second.... | |||
328 | - for weekly: Bit0=Sun, Bit6=Sat | |||
329 | - for monthly by weekday: Bit0=first Sun, Bit7=2nd Sun... Bit35=5th Sun | |||
330 | - for monthly by day: Bit0=1st, Bit1=2nd, Bit31=31st. | |||
331 | - for yearly by month: Bit0=jan, Bit1=feb,... | |||
332 | - for yearly by weeknumber: Bit0=weekno1,.... | |||
333 | ||||
334 | Offset=3: <field name="RR_LMASK" type="integer" compare="conflict"/> | |||
335 | - bit coded list of items according to frequency modifier, | |||
336 | relative to the end of the interval: | |||
337 | - Bit0=last, Bit1=second last, Bit2=third last.... | |||
338 | - for weekly: Bit0=last Sun, Bit1=last Mon,... Bit7=second last Sun,... | |||
339 | ||||
340 | Offset=4: <field name="RR_END" type="timestamp" compare="conflict"/> | |||
341 | - end date. This is calculated from DTSTART and count if incoming rule does | |||
342 | not specify an end date | |||
343 | ||||
344 | Offset=5: <field name="DTSTART" type="timestamp" compare="always"/> | |||
345 | - for reference only | |||
346 | ||||
347 | */ | |||
348 | ||||
349 | // Converts internal recurrence into vCalendar 1.0 RRULE string | |||
350 | bool internalToRRULE1( | |||
351 | string &aString, // receives RRULE string | |||
352 | char freq, | |||
353 | char freqmod, | |||
354 | sInt16 interval, | |||
355 | fieldinteger_t firstmask, | |||
356 | fieldinteger_t lastmask, | |||
357 | lineartime_t until, | |||
358 | timecontext_t untilcontext, | |||
359 | TDebugLogger *aLogP | |||
360 | ) | |||
361 | { | |||
362 | // Now do the conversion | |||
363 | string s; | |||
364 | sInt16 i,j,k; | |||
365 | fieldinteger_t m; | |||
366 | bool repshown; | |||
367 | aString.erase(); | |||
368 | // frequency and modifier | |||
369 | switch (freq) { | |||
370 | case '0' : return true; // no repetition | |||
371 | case 'D' : aString+='D'; break; | |||
372 | case 'W' : aString+='W'; break; | |||
373 | case 'M' : | |||
374 | aString+='M'; | |||
375 | if (freqmod=='W') aString+='P'; // by-(weekday)-position | |||
376 | else if (freqmod=='D') aString+='D'; // by (month)day | |||
377 | else goto incompat; | |||
378 | break; | |||
379 | case 'Y' : | |||
380 | aString+='Y'; | |||
381 | if (freqmod=='M') aString+='M'; // by month | |||
382 | // else if (freqmod=='Y') aString+='D'; // by yearday, %%%% not supported | |||
383 | else goto incompat; | |||
384 | break; | |||
385 | default : | |||
386 | goto incompat; | |||
387 | } // switch freq | |||
388 | // add interval | |||
389 | if (interval<1) goto incompat; | |||
390 | StringObjAppendPrintf(aString,"%hd",interval); | |||
391 | // add modifiers | |||
392 | switch (freqmod) { | |||
393 | case 'W' : | |||
394 | if (freq=='M') { | |||
395 | // Monthly by weekday | |||
396 | m = firstmask; | |||
397 | for (i=0; i<2; i++) { | |||
398 | // - from start and from end | |||
399 | for (j=0; j<WeeksOfMonth; j++) { | |||
400 | // - repetition | |||
401 | repshown=false; | |||
402 | // - append weekdays for this repetition | |||
403 | for (k=0; k<DaysOfWeek; k++) { | |||
404 | if (m & ((uInt64)1<<(k+(DaysOfWeek*j)))) { | |||
405 | // show repetion before first day item | |||
406 | if (!repshown) { | |||
407 | repshown=true; | |||
408 | StringObjAppendPrintf(aString," %d",j+1); | |||
409 | // - show if relative to beginning or end of month | |||
410 | if (i>0) aString+='-'; else aString+='+'; | |||
411 | } | |||
412 | // show day | |||
413 | aString+=' '; aString+=RRULE_weekdays[k]; | |||
414 | } | |||
415 | } | |||
416 | } | |||
417 | // - switch to those that are relative to the end of the month | |||
418 | m = lastmask; | |||
419 | } | |||
420 | } | |||
421 | else { | |||
422 | // weekly by weekday | |||
423 | for (k=0; k<DaysOfWeek; k++) { | |||
424 | if (firstmask & ((uInt64)1<<k)) { aString+=' '; aString+=RRULE_weekdays[k]; } | |||
425 | } | |||
426 | } | |||
427 | break; | |||
428 | case 'D' : | |||
429 | case 'M' : | |||
430 | // monthly by day number or | |||
431 | // yearly by month number | |||
432 | m = firstmask; | |||
433 | for (i=0; i<2; i++) { | |||
434 | // - show day numbers | |||
435 | for (k=0; k<32; k++) { | |||
436 | if (m & ((uInt64)1<<k)) { | |||
437 | StringObjAppendPrintf(aString," %d",k+1); | |||
438 | // show if relative to the end | |||
439 | if (i>0) aString+='-'; | |||
440 | } | |||
441 | } | |||
442 | // - switch to those that are relative to the end of the month / year | |||
443 | m = lastmask; | |||
444 | } | |||
445 | break; | |||
446 | case ' ' : | |||
447 | // no modifiers, that's ok as well | |||
448 | break; | |||
449 | default : | |||
450 | goto incompat; | |||
451 | } // switch freqmod | |||
452 | // add end date (or #0 if no end date = endless) | |||
453 | if (until!=noLinearTime) { | |||
454 | // there is an end date, use it | |||
455 | aString+=' '; | |||
456 | TimestampToISO8601Str(s, until, untilcontext); | |||
457 | aString+=s; | |||
458 | } | |||
459 | else { | |||
460 | // there is no end date, repeat forever | |||
461 | aString+=" #0"; | |||
462 | } | |||
463 | // genereated a RRULE | |||
464 | return true; | |||
465 | incompat: | |||
466 | // incompatible, cannot be shown as vCal 1.0 RRULE | |||
467 | aString.erase(); | |||
468 | return false; // no value generated | |||
469 | } // internalToRRULE1 | |||
470 | ||||
471 | ||||
472 | // Converts internal recurrence into vCalendar 2.0 RRULE string | |||
473 | bool internalToRRULE2( | |||
474 | string &aString, // receives RRULE string | |||
475 | char freq, | |||
476 | char freqmod, | |||
477 | sInt16 interval, | |||
478 | fieldinteger_t firstmask, | |||
479 | fieldinteger_t lastmask, | |||
480 | lineartime_t until, | |||
481 | timecontext_t untilcontext, | |||
482 | TDebugLogger *aLogP | |||
483 | ) | |||
484 | { | |||
485 | LOGDEBUGPRINTFX(aLogP,DBG_EXOTIC+DBG_GEN,({ if ((aLogP) && ((0x80000000 +0x00000400) & (aLogP )->getMask()) == (0x80000000 +0x00000400)) (aLogP)->setNextMask (0x80000000 +0x00000400).DebugPrintfLastMask ( "InternalToRRULE2(): expanding freq=%c, freqmod=%c, interval=%hd, firstmask=%llX, lastmask=%llX" , freq, freqmod, interval, (long long)firstmask, (long long)lastmask ); } | |||
486 | "InternalToRRULE2(): expanding freq=%c, freqmod=%c, interval=%hd, firstmask=%llX, lastmask=%llX",{ if ((aLogP) && ((0x80000000 +0x00000400) & (aLogP )->getMask()) == (0x80000000 +0x00000400)) (aLogP)->setNextMask (0x80000000 +0x00000400).DebugPrintfLastMask ( "InternalToRRULE2(): expanding freq=%c, freqmod=%c, interval=%hd, firstmask=%llX, lastmask=%llX" , freq, freqmod, interval, (long long)firstmask, (long long)lastmask ); } | |||
487 | freq, freqmod, interval, (long long)firstmask, (long long)lastmask{ if ((aLogP) && ((0x80000000 +0x00000400) & (aLogP )->getMask()) == (0x80000000 +0x00000400)) (aLogP)->setNextMask (0x80000000 +0x00000400).DebugPrintfLastMask ( "InternalToRRULE2(): expanding freq=%c, freqmod=%c, interval=%hd, firstmask=%llX, lastmask=%llX" , freq, freqmod, interval, (long long)firstmask, (long long)lastmask ); } | |||
488 | )){ if ((aLogP) && ((0x80000000 +0x00000400) & (aLogP )->getMask()) == (0x80000000 +0x00000400)) (aLogP)->setNextMask (0x80000000 +0x00000400).DebugPrintfLastMask ( "InternalToRRULE2(): expanding freq=%c, freqmod=%c, interval=%hd, firstmask=%llX, lastmask=%llX" , freq, freqmod, interval, (long long)firstmask, (long long)lastmask ); }; | |||
489 | ||||
490 | // Now do the conversion | |||
491 | string s; | |||
492 | sInt16 i,j,k; | |||
493 | fieldinteger_t m; | |||
494 | bool repshown; | |||
495 | cAppCharP sep; | |||
496 | aString.erase(); | |||
497 | // frequency and modifier | |||
498 | switch (freq) { | |||
499 | case '0' : return true; // no repetition | |||
500 | case 'D' : aString+="FREQ=DAILY"; break; | |||
501 | case 'W' : aString+="FREQ=WEEKLY"; break; | |||
502 | case 'M' : aString+="FREQ=MONTHLY"; break; | |||
503 | case 'Y' : aString+="FREQ=YEARLY"; break; | |||
504 | default : | |||
505 | goto incompat; | |||
506 | } // switch freq | |||
507 | // add interval | |||
508 | if (interval<1) goto incompat; | |||
509 | StringObjAppendPrintf(aString,";INTERVAL=%hd",interval); | |||
510 | // add modifiers | |||
511 | switch (freqmod) { | |||
512 | case 'W' : | |||
513 | sep=";BYDAY="; | |||
514 | if (freq=='M') { | |||
515 | // Monthly by weekday | |||
516 | m = firstmask; | |||
517 | for (i=0; i<2; i++) { | |||
518 | // - from start and from end | |||
519 | for (j=0; j<WeeksOfMonth; j++) { | |||
520 | // - repetition | |||
521 | repshown=false; | |||
522 | // - append weekdays for this repetition | |||
523 | for (k=0; k<DaysOfWeek; k++) { | |||
524 | if (m & ((uInt64)1<<(k+(DaysOfWeek*j)))) { | |||
525 | // show repetion before first day item | |||
526 | aString+=sep; | |||
527 | sep=","; | |||
528 | if (!repshown) { | |||
529 | repshown=true; | |||
530 | if (i>0) aString+='-'; | |||
531 | StringObjAppendPrintf(aString,"%d",j+1); | |||
532 | // - show if relative to beginning or end of month | |||
533 | } | |||
534 | // show day | |||
535 | aString+=RRULE_weekdays[k]; | |||
536 | } | |||
537 | } | |||
538 | } | |||
539 | // - switch to those that are relative to the end of the month | |||
540 | m = lastmask; | |||
541 | } | |||
542 | } | |||
543 | else { | |||
544 | // weekly by weekday | |||
545 | for (k=0; k<DaysOfWeek; k++) { | |||
546 | if (firstmask & ((uInt64)1<<k)) { | |||
547 | aString+=sep; | |||
548 | sep=","; | |||
549 | aString+=RRULE_weekdays[k]; | |||
550 | } | |||
551 | } | |||
552 | } | |||
553 | break; | |||
554 | case 'D' : | |||
555 | // monthly by day number | |||
556 | appendMaskAsNumbers(";BYMONTHDAY=", aString, firstmask, lastmask); | |||
557 | break; | |||
558 | case 'Y' : | |||
559 | // yearday by day number | |||
560 | appendMaskAsNumbers(";BYYEARDAY=", aString, firstmask, lastmask); | |||
561 | break; | |||
562 | case 'N' : | |||
563 | // yearly by week number | |||
564 | appendMaskAsNumbers(";BYWEEKNO=", aString, firstmask, lastmask); | |||
565 | break; | |||
566 | case 'M' : | |||
567 | // yearly by month number | |||
568 | appendMaskAsNumbers(";BYMONTH=", aString, firstmask, lastmask); | |||
569 | break; | |||
570 | case ' ' : | |||
571 | // no modifiers, that's ok as well | |||
572 | break; | |||
573 | default : | |||
574 | goto incompat; | |||
575 | } // switch freqmod | |||
576 | // add end date | |||
577 | // Note: for correct iCalendar 2.0, this should be (and is, in mimedir) called with untilcontext=UTC unless it's date-only | |||
578 | if (until!=noLinearTime) { | |||
579 | // there is an end date, use it | |||
580 | aString+=";UNTIL="; | |||
581 | TimestampToISO8601Str(s, until, untilcontext); | |||
582 | aString+=s; | |||
583 | } | |||
584 | ||||
585 | // do output | |||
586 | LOGDEBUGPRINTFX(aLogP,DBG_GEN,("generated rrule %s", aString.c_str())){ if ((aLogP) && ((0x00000400) & (aLogP)->getMask ()) == (0x00000400)) (aLogP)->setNextMask(0x00000400).DebugPrintfLastMask ("generated rrule %s", aString.c_str()); }; | |||
587 | ||||
588 | // genereated a RRULE | |||
589 | return true; | |||
590 | incompat: | |||
591 | // incompatible, cannot be shown as vCal 1.0 RRULE | |||
592 | aString.erase(); | |||
593 | return false; // no value generated | |||
594 | } // internalToRRULE2 | |||
595 | ||||
596 | ||||
597 | // Converts vCalendar 1.0 RRULE string into internal recurrence representation | |||
598 | bool RRULE1toInternal( | |||
599 | const char *aText, // RRULE string to be parsed | |||
600 | lineartime_t dtstart, // reference date for parsing RRULE | |||
601 | timecontext_t startcontext, // context of reference date | |||
602 | char &freq, | |||
603 | char &freqmod, | |||
604 | sInt16 &interval, | |||
605 | fieldinteger_t &firstmask, | |||
606 | fieldinteger_t &lastmask, | |||
607 | lineartime_t &until, | |||
608 | timecontext_t &untilcontext, | |||
609 | TDebugLogger *aLogP | |||
610 | ) | |||
611 | { | |||
612 | string s; | |||
613 | const char *p; | |||
614 | sInt16 startwday; | |||
615 | sInt16 startyear,startmonth,startday; | |||
616 | bool fromstart; | |||
617 | sInt16 cnt; | |||
618 | ||||
619 | // get elements of start point | |||
620 | startwday=lineartime2weekday(dtstart); // get starting weekday | |||
621 | lineartime2date(dtstart,&startyear,&startmonth,&startday); // year, month, day-in-month | |||
622 | // do the conversion here | |||
623 | p=aText; | |||
624 | char c,c2; | |||
625 | sInt16 j,k; | |||
626 | fieldinteger_t m; | |||
627 | // get frequency and modifier | |||
628 | do c=*p++; while(c==' '); // next non-space | |||
629 | if (c==0) goto incompat; // no frequency: incompatible -> parse error | |||
630 | switch (c) { | |||
631 | case 'D' : freq='D'; break; | |||
632 | case 'W' : freq='W'; freqmod='W'; break; | |||
633 | case 'M' : | |||
634 | freq='M'; | |||
635 | // get modifier | |||
636 | c=*p++; | |||
637 | if (c=='P') freqmod='W'; // by weekday | |||
638 | else if (c=='D') freqmod='D'; // by monthday | |||
639 | else goto norep; // unknown modifier: cancel repetition (but no error) | |||
640 | break; | |||
641 | case 'Y' : | |||
642 | freq='Y'; | |||
643 | // get modifier | |||
644 | c=*p++; | |||
645 | if (c=='M') freqmod='M'; // by monthlist | |||
646 | else if (c=='D') freqmod='Y'; // by day-of-year list %%% not supported | |||
647 | else goto norep; // unknown modifier: cancel repetition (but no error) | |||
648 | break; | |||
649 | default : | |||
650 | goto incompat; | |||
651 | } | |||
652 | // get interval | |||
653 | while (isdigit(*p)) { interval=interval*10+((*p++)-'0'); } | |||
654 | if (interval==0) goto incompat; // no interval is incompatible -> parse error | |||
655 | // get modifier(s) if any | |||
656 | switch (freq) { | |||
657 | case 'W' : | |||
658 | // weekly may or may not have modifiers | |||
659 | do { | |||
660 | do c=*p++; while(c==' '); // next non-space | |||
661 | if (isalpha(c)) { | |||
662 | c2=*p++; | |||
663 | if (!isalpha(c2)) goto incompat; // bad weekday syntax -> parse error | |||
664 | for (k=0;k<DaysOfWeek;k++) if (toupper(c)==RRULE_weekdays[k][0] && toupper(c2)==RRULE_weekdays[k][1]) break; | |||
665 | if (k==DaysOfWeek) goto incompat; // bad weekday -> parse error | |||
666 | firstmask = firstmask | ((uInt64)1<<k); // add weekday | |||
667 | } | |||
668 | else { | |||
669 | p--; | |||
670 | break; | |||
671 | } | |||
672 | } while(true); | |||
673 | // make sure start day is always in mask | |||
674 | if (dtstart) firstmask |= ((uInt64)1<<startwday); | |||
675 | break; | |||
676 | case 'M' : | |||
677 | fromstart=true; // default to specification relative to start | |||
678 | if (freqmod=='W') { | |||
679 | // monthly by weekday (position) | |||
680 | j=0; // default to first occurrence until otherwise set | |||
681 | do { | |||
682 | do c=*p++; while(c==' '); // next non-space | |||
683 | if (isdigit(c) && !isdigit(*p)) { // only single digit, otherwise it might be end date | |||
684 | // get occurrence specs | |||
685 | if (c>'5' || c<'1') goto incompat; // no more than 5 weeks in a month! -> parse error | |||
686 | j=c-'1'; // first occurrence=0 | |||
687 | // check for '+' or '-' | |||
688 | if (*p=='+') p++; // simply skip | |||
689 | else if (!(fromstart=!(*p=='-'))) p++; // skip | |||
690 | } | |||
691 | else if (isalpha(c)) { | |||
692 | // get weekday spec | |||
693 | c2=*p++; | |||
694 | if (!isalpha(c2)) goto incompat; // bad weekday syntax -> parse error | |||
695 | for (k=0;k<DaysOfWeek;k++) if (toupper(c)==RRULE_weekdays[k][0] && toupper(c2)==RRULE_weekdays[k][1]) break; | |||
696 | if (k==DaysOfWeek) goto incompat; // bad weekday -> parse error | |||
697 | m=(uInt64)1<<(DaysOfWeek*j+k); | |||
698 | if (fromstart) firstmask |= m; else lastmask |= m; // add weekday rep | |||
699 | } | |||
700 | else { | |||
701 | // end of modifiers | |||
702 | p--; | |||
703 | break; | |||
704 | } | |||
705 | } while(true); | |||
706 | // check if we need defaults | |||
707 | if (!(firstmask | lastmask)) { | |||
708 | if (!dtstart) goto norep; // cannot set, no start date (but no error) | |||
709 | // determine the repetition in the month of this weekday | |||
710 | j=(startday-1) / DaysOfWeek; | |||
711 | firstmask = ((uInt64)1<<(DaysOfWeek*j+startwday)); // set nth repetition of current weekday | |||
712 | } | |||
713 | } | |||
714 | else { | |||
715 | // must be 'D' = monthly by day of month | |||
716 | do { | |||
717 | fromstart=true; | |||
718 | do c=*p++; while(c==' '); // next non-space | |||
719 | if (c=='L' && *p=='D') { | |||
720 | // special case: "LD" means last day of month | |||
721 | lastmask |= 1; | |||
722 | } | |||
723 | else if (isdigit(c) && (*p) && (!isdigit(*p) || !isdigit(*(p+1)))) { // more than two digits are end date | |||
724 | // get day number | |||
725 | k=c-'0'; | |||
726 | if (isdigit(*p)) k=k*10+((*p++)-'0'); | |||
727 | // check for '+' or '-' | |||
728 | if (*p=='+') p++; // simply skip plus | |||
729 | if (!(fromstart=!(*p=='-'))) p++; // skip minus and set fromstart to false | |||
730 | if (k==0) goto incompat; // 0 is not allowed -> parse error | |||
731 | // set mask | |||
732 | k--; // bits are 0 based | |||
733 | m = (uInt64)1<<k; | |||
734 | if (fromstart) firstmask |= m; else lastmask |= m; // add day | |||
735 | } | |||
736 | else { | |||
737 | p--; | |||
738 | break; | |||
739 | } | |||
740 | } while(true); | |||
741 | // check if we need defaults | |||
742 | if (!(firstmask | lastmask)) { | |||
743 | if (!dtstart) goto norep; // cannot set, no start date (no error) | |||
744 | // set current day number | |||
745 | firstmask = ((uInt64)1<<(startday-1)); // set bit of current day-in-month | |||
746 | } | |||
747 | } // by day of month | |||
748 | // monthly modifiers done | |||
749 | break; | |||
750 | case 'Y' : | |||
751 | if (freqmod=='M') { | |||
752 | // by monthlist | |||
753 | do { | |||
754 | do c=*p++; while (c==' '); // next non-space | |||
755 | if (isdigit(c) && (*p) && (!isdigit(*p) || !isdigit(*(p+1)))) { // more than two digits are end date | |||
756 | // get month number | |||
757 | k=c-'0'; | |||
758 | if (isdigit(*p)) k=k*10+((*p++)-'0'); | |||
759 | if (k==0) goto incompat; // 0 is not allowed -> parse error | |||
760 | // set mask | |||
761 | k--; // bits are 0 based | |||
762 | firstmask |= (uInt64)1<<k; // add month | |||
763 | } | |||
764 | else { | |||
765 | p--; | |||
766 | break; | |||
767 | } | |||
768 | } while(true); | |||
769 | // check if we need defaults | |||
770 | if (!firstmask) { | |||
771 | if (!dtstart) goto norep; // cannot set, no start date (but no error) | |||
772 | // set current month number | |||
773 | firstmask = ((uInt64)1<<(startmonth-1)); // set bit of current day-in-month | |||
774 | } | |||
775 | } | |||
776 | else { | |||
777 | // by day of year, only supported if no list follows | |||
778 | // - check if list follows | |||
779 | do c=*p++; while (c==' '); // next non-space | |||
780 | if (isdigit(c)) { | |||
781 | if (*p) { | |||
782 | if (!isdigit(*p)) goto norep; // single digit, is list, abort (but no error) | |||
783 | if (*(p+1)) { | |||
784 | if (!isdigit(*(p+1))) goto norep; // dual digit, is list, abort (but no error) | |||
785 | if (*(p+2)) { | |||
786 | if (!isdigit(*(p+2))) goto norep; // three digit, is list, abort (but no error) | |||
787 | } | |||
788 | } | |||
789 | } | |||
790 | } | |||
791 | // if we get so far, it's either end date (>= 4 digits) or # or end of string | |||
792 | --p; // we have fetched one to many | |||
793 | // - make best try to convert to by-monthlist | |||
794 | freqmod='M'; | |||
795 | firstmask = ((uInt64)1<<(startmonth-1)); // set bit of current day-in-month | |||
796 | } | |||
797 | // yearly modifiers done | |||
798 | break; | |||
799 | } // switch for modifier reading | |||
800 | // get count or end date | |||
801 | do c=*p++; while (c==' '); // next non-space | |||
802 | cnt=2; // default to repeat once (=applied two times) | |||
803 | if (c==0) goto calcenddate; | |||
804 | else if (c=='#') { | |||
805 | // count specified | |||
806 | if (!StrToShort(p,cnt)) goto incompat; // bad count -> parse error | |||
807 | calcenddate: | |||
808 | if (!endDateFromCount(until,dtstart,freq,freqmod,interval,firstmask,lastmask,cnt,false,aLogP)) | |||
809 | goto norep; | |||
810 | untilcontext = startcontext; // until is in same context as start | |||
811 | } // count specified | |||
812 | else { | |||
813 | // must be end date, or spec is bad | |||
814 | p--; | |||
815 | if (ISO8601StrToTimestamp(p, until, untilcontext)==0) | |||
816 | goto incompat; // bad end date -> parse error | |||
817 | } // end date specified | |||
818 | // parsed ok, now store it | |||
819 | goto store; | |||
820 | norep: | |||
821 | // no repetition (but no parse error generated) | |||
822 | freq='0'; // frequency = none | |||
823 | freqmod=' '; // frequency modifier | |||
824 | interval=0; // interval | |||
825 | firstmask=0; // day mask counted from the first day of the period | |||
826 | lastmask=0; // day mask counted from the last day of the period | |||
827 | until= noLinearTime; // last day | |||
828 | store: | |||
829 | return true; // ok | |||
830 | incompat: | |||
831 | // incompatible, value cannot be parsed usefully | |||
832 | return false; // no value generated | |||
833 | } // RRULE1toInternal | |||
834 | ||||
835 | ||||
836 | /// @brief calculate end date of RRULE when count is specified | |||
837 | /// @return true if repeating, false if not repeating at all | |||
838 | /// @note returns until=noLinearTime for endless repeat (count=0) | |||
839 | bool endDateFromCount( | |||
840 | lineartime_t &until, | |||
841 | lineartime_t dtstart, | |||
842 | char freq, char freqmod, | |||
843 | sInt16 interval, | |||
844 | fieldinteger_t firstmask,fieldinteger_t lastmask, | |||
845 | sInt16 cnt, bool countsoccurrences, | |||
846 | TDebugLogger *aLogP | |||
847 | ) | |||
848 | { | |||
849 | // count<=0 means endless | |||
850 | if (cnt<=0) { | |||
851 | until= noLinearTime; // forever, we don't need a start date for this | |||
852 | return true; // ok | |||
853 | } | |||
854 | // check no-rep cases | |||
855 | if (cnt<0) return false; // negative count, no repeat | |||
856 | if (dtstart==noLinearTime) return false; // no start date, cannot calc end date -> no rep (but no error) | |||
857 | if (interval<=0) return false; // interval=0 means no recurrence (but no error) | |||
858 | // default to dtstart | |||
859 | until = dtstart; | |||
860 | // calculate elements of start point | |||
861 | sInt16 startwday; | |||
862 | sInt16 startyear,startmonth,startday; | |||
863 | lineartime_t starttime; | |||
864 | starttime = lineartime2timeonly(dtstart); // start time of day | |||
865 | startwday = lineartime2weekday(dtstart); // get starting weekday | |||
866 | lineartime2date(dtstart,&startyear,&startmonth,&startday); // year, month, day-in-month | |||
867 | // calculate interval repetitions (which is what is needed for daily and RRULE v1 calculation) | |||
868 | sInt16 ivrep = (cnt-1)*interval; | |||
869 | // check if daily | |||
870 | if (freq == 'D') { | |||
871 | // Daily recurrence is same for occurrence and interval counts | |||
872 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: daily calc - same in all cases")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: daily calc - same in all cases" ); }; | |||
873 | until = dtstart+(ivrep*linearDateToTimeFactor); | |||
874 | return true; | |||
875 | } | |||
876 | else if (!countsoccurrences) { | |||
877 | // RRULE v1 interpretation of count (=number of repetitions of interval, not number of occurrences) | |||
878 | // - determine end of recurrence interval (which is usually NOT a occurrence precisely) | |||
879 | switch (freq) | |||
880 | { | |||
881 | case 'W': | |||
882 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: simple weekly calc")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: simple weekly calc" ); }; | |||
883 | // v1 end date calc, we need to take into account possible masks, so result must be end of interval, not just start date+interval | |||
884 | // weekly: end date is last day of target week | |||
885 | until=dtstart+((ivrep*DaysOfWeek-startwday+6)*linearDateToTimeFactor); | |||
886 | return true; | |||
887 | case 'M': | |||
888 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: simple monthly calc")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: simple monthly calc" ); }; | |||
889 | startmonth--; // make 0 based | |||
890 | startmonth += ivrep+1; // add number of months plus one (as we want next month, and then go one day back to last day of month) | |||
891 | startyear += startmonth / 12; // update years | |||
892 | startmonth = startmonth % 12 + 1; // update month and make 1 based again | |||
893 | // - calculate last day in month of occurrence | |||
894 | until = (date2lineardate(startyear,startmonth,1)-1)*linearDateToTimeFactor+starttime; | |||
895 | return true; | |||
896 | case 'Y': | |||
897 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: simple yearly calc")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: simple yearly calc" ); }; | |||
898 | // yearly: end date is end of end year | |||
899 | until = (date2lineardate(startyear+ivrep,12,31))*linearDateToTimeFactor+starttime; | |||
900 | return true; | |||
901 | } | |||
902 | } | |||
903 | else { | |||
904 | // v2 type counting, means that count specifies number of occurrences, not interval repetitions | |||
905 | // requires more elaborate expansion | |||
906 | // NOTE: this does not work without masks set, so we need to calculate the default masks if none are explicitly set | |||
907 | // - we need the number of days in the month in most cases | |||
908 | sInt16 lastday = getMonthDays(lineartime2dateonly(dtstart)); // number of days in this month | |||
909 | sInt16 newYearsPassed; | |||
910 | switch (freq) | |||
911 | { | |||
912 | case 'W': | |||
913 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: full expansion weekly calc")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: full expansion weekly calc" ); }; | |||
914 | // - make sure we have a mask | |||
915 | if (firstmask==0 && lastmask==0) | |||
916 | firstmask = 1<<startwday; // set start day in mask | |||
917 | // now calculate | |||
918 | while (firstmask) { | |||
919 | if (firstmask & ((uInt64)1<<startwday)) { | |||
920 | // found an occurrence | |||
921 | cnt--; // count it | |||
922 | if (cnt<=0) break; // found all | |||
923 | } | |||
924 | // increment day | |||
925 | until+=linearDateToTimeFactor; | |||
926 | startwday++; | |||
927 | if (startwday>6) { | |||
928 | // new week starts | |||
929 | startwday=0; | |||
930 | // skip part of interval which has no occurrence | |||
931 | until+=(interval-1)*7*linearDateToTimeFactor; | |||
932 | } | |||
933 | } | |||
934 | return true; | |||
935 | case 'M': | |||
936 | if (freqmod=='W') { | |||
937 | // monthly by weekday | |||
938 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: full expansion of monthly by weekday")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: full expansion of monthly by weekday" ); }; | |||
939 | // - make sure we have a mask | |||
940 | if (firstmask==0 && lastmask==0) | |||
941 | firstmask = (uInt64)1<<(startwday+7*((startday-1)/7)); // set start day in mask | |||
942 | // - now calculate | |||
943 | while (firstmask || lastmask) { | |||
944 | // calculate which weeks we are in | |||
945 | sInt16 fwk=(startday-1) / 7; // start is nth week of the month | |||
946 | sInt16 lwk=(lastday-startday) / 7; // start is nth-last week of the month | |||
947 | if ( | |||
948 | (firstmask & ((uInt64)1<<(startwday+7*fwk))) || // nth occurrence of weekday in month | |||
949 | (lastmask & ((uInt64)1<<(startwday+7*lwk))) // nth-last occurrence of weekday in month | |||
950 | ) { | |||
951 | // found an occurrence | |||
952 | cnt--; // count it | |||
953 | if (cnt<=0) break; // found all | |||
954 | } | |||
955 | // increment day | |||
956 | until+=linearDateToTimeFactor; | |||
957 | startday++; // next day in month | |||
958 | startwday++; if (startwday>6) startwday=0; // next day in the week | |||
959 | // check for new month | |||
960 | if (startday>lastday) { | |||
961 | // new month starts | |||
962 | sInt16 i=interval; | |||
963 | while (true) { | |||
964 | lastday = getMonthDays(lineartime2dateonly(until)); // number of days in next month | |||
965 | startday = 1; // start at 1st of month again | |||
966 | if (--i == 0) break; // done | |||
967 | // skip entire next month | |||
968 | until+=lastday*linearDateToTimeFactor; // advance by number of days in this month | |||
969 | } | |||
970 | // now recalculate weekday | |||
971 | startwday=lineartime2weekday(until); // calculation continues here | |||
972 | } | |||
973 | } | |||
974 | } | |||
975 | else { | |||
976 | // everything else, including no modifier, is treated as monthly by monthday | |||
977 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: full expansion of monthly by monthday")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: full expansion of monthly by monthday" ); }; | |||
978 | // - make sure we have a mask | |||
979 | if (firstmask==0 && lastmask==0) | |||
980 | firstmask = (uInt64)1<<(startday-1); // set start day in mask | |||
981 | // - now calculate | |||
982 | while (firstmask || lastmask) { | |||
983 | if ( | |||
984 | (firstmask & ((uInt64)1<<(startday-1))) || // nth day in month | |||
985 | (lastmask & ((uInt64)1<<(lastday-startday))) // nth-last day in month | |||
986 | ) { | |||
987 | // found an occurrence | |||
988 | cnt--; // count it | |||
989 | if (cnt<=0) break; // found all | |||
990 | } | |||
991 | // increment day | |||
992 | until+=linearDateToTimeFactor; | |||
993 | startday++; // next day in month | |||
994 | if (startday>lastday) { | |||
995 | // new month starts | |||
996 | sInt16 i=interval; | |||
997 | while (true) { | |||
998 | lastday = getMonthDays(lineartime2dateonly(until)); // number of days in next month | |||
999 | startday = 1; // start at 1st of month again | |||
1000 | if (--i == 0) break; // done | |||
1001 | // skip entire next month | |||
1002 | until+=lastday*linearDateToTimeFactor; // advance by number of days in this month | |||
1003 | } | |||
1004 | } | |||
1005 | } | |||
1006 | } | |||
1007 | return true; | |||
1008 | case 'Y': | |||
1009 | if (freqmod=='M') { | |||
1010 | // Yearly by month | |||
1011 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: full expansion of yearly by month")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: full expansion of yearly by month" ); }; | |||
1012 | // - make sure we have a mask | |||
1013 | if (firstmask==0 && lastmask==0) | |||
1014 | firstmask = (uInt64)1<<(startmonth-1); // set start month in mask | |||
1015 | // - do entire calculation on 1st of month such that we can be sure that day exists (unlike a Feb 30th or April 31th) | |||
1016 | until -= (startday-1)*linearDateToTimeFactor; | |||
1017 | // - now calculate | |||
1018 | newYearsPassed=0; | |||
1019 | // occurrence interval can be at most 4 years in the future (safety abort) | |||
1020 | while (newYearsPassed<=4) { | |||
1021 | if (firstmask & ((uInt64)1<<(startmonth-1))) { | |||
1022 | // possibly found an occurrence | |||
1023 | // - is an occurrence only if that day exists in the month | |||
1024 | if (startday<=lastday) { | |||
1025 | cnt--; // count it | |||
1026 | newYearsPassed = 0; | |||
1027 | if (cnt<=0) break; // found all | |||
1028 | } | |||
1029 | } | |||
1030 | // go to same day in next month (and skip months that don't have that day, like an 31st April or 30Feb | |||
1031 | until+=lastday*linearDateToTimeFactor; | |||
1032 | startmonth++; | |||
1033 | if (startmonth>12) { | |||
1034 | // new year starts | |||
1035 | startmonth=1; | |||
1036 | newYearsPassed++; | |||
1037 | // skip additional years (in month steps) | |||
1038 | for (sInt16 i=(interval-1)*12; i>0; i--) { | |||
1039 | lastday = getMonthDays(lineartime2dateonly(until)); // number of days in next month | |||
1040 | // skip month | |||
1041 | until+=lastday*linearDateToTimeFactor; // advance by number of days in this month | |||
1042 | } | |||
1043 | } | |||
1044 | // get size of next month to check | |||
1045 | lastday = getMonthDays(lineartime2dateonly(until)); // number of days in next month | |||
1046 | } | |||
1047 | // move back to start day | |||
1048 | until += (startday-1)*linearDateToTimeFactor; | |||
1049 | } | |||
1050 | else { | |||
1051 | // everything else, including no modifier, is treated as yearly on the same date (multiple occurrences per year not supported) | |||
1052 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("endDateFromCount: full expansion of yearly by yearday - NOT SUPPORTED with more than one day")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("endDateFromCount: full expansion of yearly by yearday - NOT SUPPORTED with more than one day" ); }; | |||
1053 | until = (date2lineardate(startyear+ivrep,startmonth,startday))*linearDateToTimeFactor+starttime; | |||
1054 | } | |||
1055 | return true; | |||
1056 | } // switch | |||
1057 | } | |||
1058 | // no recurrence | |||
1059 | return false; | |||
1060 | } // endDateFromCount | |||
1061 | ||||
1062 | ||||
1063 | ||||
1064 | #ifdef DEBUG | |||
1065 | /* | |||
1066 | // %%%% hack debug | |||
1067 | #define SYNTHESIS_UNIT_TEST 1 | |||
1068 | #define UNIT_TEST_TITLE(a) | |||
1069 | #define UNIT_TEST_CALL(x,p,t,v) | |||
1070 | */ | |||
1071 | #endif | |||
1072 | ||||
1073 | #ifdef SYNTHESIS_UNIT_TEST | |||
1074 | ||||
1075 | // helper to create lineartime parameters | |||
1076 | static lineartime_t t(char *aTime) | |||
1077 | { | |||
1078 | if (!aTime || *aTime==0) | |||
1079 | return noLinearTime; | |||
1080 | timecontext_t tctx; | |||
1081 | lineartime_t res; | |||
1082 | ISO8601StrToTimestamp(aTime, res, tctx); | |||
1083 | return res; | |||
1084 | } | |||
1085 | ||||
1086 | // helper to show lineartime results | |||
1087 | static char buf[100]; | |||
1088 | char *s(lineartime_t aTime) | |||
1089 | { | |||
1090 | if (aTime==noLinearTime) | |||
1091 | return "<none>"; | |||
1092 | string str; | |||
1093 | TimestampToISO8601Str(str, aTime, TCTX_UNKNOWN((timecontext_t) ((tctx_tz_unknown) | TCTX_SYMBOLIC_TZ)), true, false); | |||
1094 | strcpy(buf,str.c_str()); | |||
1095 | return buf; | |||
1096 | } | |||
1097 | ||||
1098 | ||||
1099 | // RRULE expansion tests | |||
1100 | bool test_expand_rrule(void) | |||
1101 | { | |||
1102 | bool ok=true; | |||
1103 | lineartime_t lt; | |||
1104 | TRRuleExpandStatus es; | |||
1105 | ||||
1106 | { | |||
1107 | UNIT_TEST_TITLE("Birthday 1"); | |||
1108 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2008-03-31T00:00:00"),'Y','M',1,0x4,0x0,t("2009-03-31T00:00:00"),t("2009-04-01T00:00:00")),("-none-"),true,ok); | |||
1109 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-31T00:00:00"),ok); | |||
1110 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 3) | |||
1111 | ||||
1112 | UNIT_TEST_TITLE("Birthday 2"); | |||
1113 | UNIT_TEST_CALL(initRRuleExpansion(es,t("1979-03-06T00:00:00"),'Y','M',1,0x4,0x0,t("2009-03-31T00:00:00"),t("2009-04-01T00:00:00")),("-none-"),true,ok); | |||
1114 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // no occurrences (checked 32) | |||
1115 | ||||
1116 | UNIT_TEST_TITLE("Birthday 3"); | |||
1117 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2006-03-13T00:00:00"),'Y','M',1,0x4,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1118 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-13T00:00:00"),ok); | |||
1119 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 5) | |||
1120 | ||||
1121 | UNIT_TEST_TITLE("Pay day"); | |||
1122 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2006-06-20T00:00:00"),'M','D',1,0x80000,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1123 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-20T00:00:00"),ok); | |||
1124 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 35) | |||
1125 | ||||
1126 | UNIT_TEST_TITLE("St. Patrick's Day"); | |||
1127 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2006-03-17T00:00:00"),'Y','M',1,0x4,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1128 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-17T00:00:00"),ok); | |||
1129 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 5) | |||
1130 | ||||
1131 | // Expanding 'Change fish tank filter': | |||
1132 | UNIT_TEST_TITLE("Change fish tank filter"); | |||
1133 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2007-10-21T00:00:00"),'W','W',5,0x1,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1134 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-29T00:00:00"),ok); | |||
1135 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 17) | |||
1136 | ||||
1137 | // Expanding 'Start of British Summer Time': | |||
1138 | UNIT_TEST_TITLE("Start of British Summer Time"); | |||
1139 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2003-03-30T00:00:00"),'M','W',12,0x0,0x1,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1140 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-29T00:00:00"),ok); | |||
1141 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 8) | |||
1142 | ||||
1143 | UNIT_TEST_TITLE("Birthday 4"); | |||
1144 | UNIT_TEST_CALL(initRRuleExpansion(es,t("1957-03-14T00:00:00"),'Y','M',1,0x4,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1145 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-14T00:00:00"),ok); | |||
1146 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 54) | |||
1147 | ||||
1148 | UNIT_TEST_TITLE("Buy Lottery tickets"); | |||
1149 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2007-08-11T00:00:00"),'W','W',3,0x40,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1150 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-02-28T00:00:00"),ok); | |||
1151 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-21T00:00:00"),ok); | |||
1152 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 30) | |||
1153 | ||||
1154 | UNIT_TEST_TITLE("Recycling bin collection"); | |||
1155 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2007-08-17T00:00:00"),'W','W',2,0x20,0x0,t("2009-02-23T00:00:00"),t("2009-03-31T00:00:00")),("-none-"),true,ok); | |||
1156 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-02-27T00:00:00"),ok); | |||
1157 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-13T00:00:00"),ok); | |||
1158 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-03-27T00:00:00"),ok); | |||
1159 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); // generated occurrences (checked 44) | |||
1160 | ||||
1161 | UNIT_TEST_TITLE("test feb 29th"); | |||
1162 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2008-02-29T14:00:00"),'Y','M',1,0x0,0x0,t("2009-04-01T00:00:00"),t("2019-04-04T00:00:00")),("-none-"),true,ok); | |||
1163 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2012-02-29T14:00:00"),ok); | |||
1164 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2016-02-29T14:00:00"),ok); | |||
1165 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); | |||
1166 | ||||
1167 | UNIT_TEST_TITLE("February and March 29th every year"); | |||
1168 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2008-02-29T14:00:00"),'Y','M',1,0x6,0x0,t("2009-04-01T00:00:00"),t("2016-04-04T00:00:00")),("-none-"),true,ok); | |||
1169 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2010-03-29T14:00:00"),ok); | |||
1170 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2011-03-29T14:00:00"),ok); | |||
1171 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2012-02-29T14:00:00"),ok); // leap year | |||
1172 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2012-03-29T14:00:00"),ok); | |||
1173 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2013-03-29T14:00:00"),ok); | |||
1174 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2014-03-29T14:00:00"),ok); | |||
1175 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2015-03-29T14:00:00"),ok); | |||
1176 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2016-02-29T14:00:00"),ok); // leap year | |||
1177 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2016-03-29T14:00:00"),ok); | |||
1178 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); | |||
1179 | ||||
1180 | UNIT_TEST_TITLE("suddenly expanding from start problem"); | |||
1181 | UNIT_TEST_CALL(initRRuleExpansion(es,t("2007-08-17"),'W','W',2,0x20,0x0,t("2009-07-04"),t("2009-08-08")),("-none-"),true,ok); | |||
1182 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-07-17"),ok); | |||
1183 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t("2009-07-31"),ok); | |||
1184 | UNIT_TEST_CALL(lt = getNextOccurrence(es),("lt = %s",s(lt)),lt==t(""),ok); | |||
1185 | ||||
1186 | } | |||
1187 | return ok; | |||
1188 | } | |||
1189 | ||||
1190 | #endif // SYNTHESIS_UNIT_TEST | |||
1191 | ||||
1192 | ||||
1193 | ||||
1194 | /// @brief initialize expansion of RRule | |||
1195 | void initRRuleExpansion( | |||
1196 | TRRuleExpandStatus &es, | |||
1197 | lineartime_t aDtstart, | |||
1198 | char aFreq, char aFreqmod, | |||
1199 | sInt16 aInterval, | |||
1200 | fieldinteger_t aFirstmask, fieldinteger_t aLastmask, | |||
1201 | lineartime_t aExpansionStart, | |||
1202 | lineartime_t aExpansionEnd | |||
1203 | ) | |||
1204 | { | |||
1205 | // %%% hardcoded for now | |||
1206 | es.weekstart = 0; // starts on sunday for now | |||
1207 | // save recurrence parameters | |||
1208 | es.freq = aFreq; | |||
1209 | es.freqmod = aFreqmod; | |||
1210 | es.interval = aInterval; | |||
1211 | es.firstmask = aFirstmask; | |||
1212 | es.lastmask = aLastmask; | |||
1213 | // analyze bits | |||
1214 | es.singleFMaskBit = -1; | |||
1215 | if (es.lastmask==0 && es.firstmask) { | |||
1216 | fieldinteger_t m = es.firstmask; | |||
1217 | es.singleFMaskBit = 0; | |||
1218 | while((m & 1)==0) { m>>=1; es.singleFMaskBit++; } // calculate bit number of next set bit | |||
1219 | if (m & ~1) es.singleFMaskBit = -1; // more bits to come - reset again | |||
1220 | } | |||
1221 | // init expansion parameters | |||
1222 | // - no valid occurrence yet | |||
1223 | es.started = false; | |||
1224 | // - elements of start point | |||
1225 | es.starttime = lineartime2timeonly(aDtstart); // start time of day | |||
1226 | lineartime2date(aDtstart,&es.startyear,&es.startmonth,&es.startday); // year, month, day-in-month | |||
1227 | // - cursor, initialized to start point | |||
1228 | es.cursor = aDtstart; | |||
1229 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1230 | es.cursorWDay = lineartime2weekday(es.cursor); // get cursor weekday | |||
1231 | // save expansion range | |||
1232 | es.expansionEnd = aExpansionEnd; // can be noLinearTime if no end is set | |||
1233 | es.expansionStartDayOffset = 0; | |||
1234 | if (aExpansionStart!=noLinearTime) { | |||
1235 | // specified start of expansion period, apply if later than beginning of recurrence | |||
1236 | es.expansionStartDayOffset = (lineartime2dateonly(aExpansionStart)-lineartime2dateonly(es.cursor)); | |||
1237 | if (es.expansionStartDayOffset<0) | |||
1238 | es.expansionStartDayOffset = 0; | |||
1239 | } | |||
1240 | #ifdef SYNTHESIS_UNIT_TEST | |||
1241 | sInt16 y,m,d,h,mi,s,ms; | |||
1242 | lineartime2date(aExpansionStart, &y, &m, &d); | |||
1243 | lineartime2time(aExpansionStart, &h, &mi, &s, &ms); | |||
1244 | printf(" aExpansionStart = %04hd-%02hd-%02hd %02hd:%02hd:%02hd - es.expansionStartDayOffset=%ld\n",y,m,d,h,mi,s,es.expansionStartDayOffset); | |||
1245 | #endif | |||
1246 | } | |||
1247 | ||||
1248 | ||||
1249 | // advance cursor by given number of days | |||
1250 | static void adjustCursor(TRRuleExpandStatus &es, lineardate_t aDays) | |||
1251 | { | |||
1252 | // now days = number of days to advance | |||
1253 | es.cursor += aDays*linearDateToTimeFactor; | |||
1254 | // adjust weekday | |||
1255 | es.cursorWDay += aDays % DaysPerWk; | |||
1256 | if (es.cursorWDay<0) es.cursorWDay += DaysPerWk; | |||
1257 | else if (es.cursorWDay>=DaysPerWk) es.cursorWDay -= DaysPerWk; | |||
1258 | #ifdef SYNTHESIS_UNIT_TEST | |||
1259 | sInt16 y,m,d,h,mi,s,ms; | |||
1260 | lineartime2date(es.cursor, &y, &m, &d); | |||
1261 | lineartime2time(es.cursor, &h, &mi, &s, &ms); | |||
1262 | printf(" es.cursor = %04hd-%02hd-%02hd %02hd:%02hd:%02hd / Day=%hd, lt=%lld\n",y,m,d,h,mi,s,es.cursorWDay,es.cursor); | |||
1263 | #endif | |||
1264 | } | |||
1265 | ||||
1266 | ||||
1267 | static lineardate_t makeMultiple(lineardate_t d, sInt16 aMultiple, sInt16 aMaxRemainder=0) | |||
1268 | { | |||
1269 | if (aMultiple>1) { | |||
1270 | // we need to make sure remainder is 0 and advance extra | |||
1271 | lineardate_t r = d % aMultiple; // remainder | |||
1272 | if (r>aMaxRemainder) { | |||
1273 | // round up to next multiple | |||
1274 | d += aMultiple - r; | |||
1275 | } | |||
1276 | } | |||
1277 | return d; | |||
1278 | } | |||
1279 | ||||
1280 | ||||
1281 | // simple and fast cursor increment | |||
1282 | static void incCursor(TRRuleExpandStatus &es) | |||
1283 | { | |||
1284 | es.cursor += linearDateToTimeFactor; | |||
1285 | es.cursorWDay += 1; | |||
1286 | if (es.cursorWDay>=DaysPerWk) es.cursorWDay=0; | |||
1287 | } | |||
1288 | ||||
1289 | ||||
1290 | static bool expansionEnd(TRRuleExpandStatus &es) | |||
1291 | { | |||
1292 | if (es.expansionEnd && es.cursor>=es.expansionEnd) { | |||
1293 | // end of expansion, reset cursor | |||
1294 | es.cursor = noLinearTime; | |||
1295 | return true; | |||
1296 | } | |||
1297 | // not yet end of expansion | |||
1298 | return false; | |||
1299 | } | |||
1300 | ||||
1301 | ||||
1302 | ||||
1303 | ||||
1304 | ||||
1305 | static sInt16 monthDiff(sInt16 y1, sInt16 m1, sInt16 y2, sInt16 m2) | |||
1306 | { | |||
1307 | return (y1-y2)*12 + (m1-m2); | |||
1308 | } | |||
1309 | ||||
1310 | ||||
1311 | static void monthAdd(sInt16 &y, sInt16 &m, sInt16 a) | |||
1312 | { | |||
1313 | m += a; | |||
1314 | if (m>12) { | |||
1315 | sInt16 r = (m-1)/12; // years plus | |||
1316 | y += r; // add the years | |||
1317 | m -= (r*12); // remove from the months | |||
1318 | } | |||
1319 | } | |||
1320 | ||||
1321 | ||||
1322 | ||||
1323 | ||||
1324 | /// @brief get next occurrence | |||
1325 | /// @return noLinearTime if no next occurrence exists, lineartime of next occurrence otherwise | |||
1326 | lineartime_t getNextOccurrence(TRRuleExpandStatus &es) | |||
1327 | { | |||
1328 | if (es.cursor==noLinearTime) return noLinearTime; // already done (or never started properly) | |||
1329 | if (es.interval<=0) return noLinearTime; // interval=0 means no recurrence (but no error) | |||
1330 | // if we are already started, we need to advance the cursor first, then check for match | |||
1331 | bool advanceFirst = es.started; | |||
1332 | // now calculate | |||
1333 | if (es.freq=='D') { | |||
1334 | // Daily: can be calculated directly | |||
1335 | if (!es.started) { | |||
1336 | // calculate first occurrence | |||
1337 | adjustCursor(es, makeMultiple(es.expansionStartDayOffset, es.interval)); // move cursor to first day | |||
1338 | } | |||
1339 | else { | |||
1340 | // calculate next occurrence | |||
1341 | adjustCursor(es, es.interval); | |||
1342 | } | |||
1343 | } // D | |||
1344 | else if (es.freq=='W') { | |||
1345 | // Note: interval of weekly recurrences starts at sunday, | |||
1346 | // so recurrence starting at a Thu, scheduled for every two weeks on Mo and Thu will have: | |||
1347 | // 1st week: Thu, 2nd week: nothing, 3rd week: Mo,Thu, 4rd week: nothing, 5th week: Mo,Thu... | |||
1348 | if (!es.started) { | |||
1349 | // make sure we have a mask | |||
1350 | if (es.firstmask==0) | |||
1351 | es.firstmask = 1<<es.cursorWDay; // set start day in mask | |||
1352 | // advance cursor to first day of expansion period | |||
1353 | // - go back to start of very first week covered by the recurrence | |||
1354 | sInt16 woffs = es.cursorWDay-es.weekstart; // how many days back to next week start from dtstart | |||
1355 | if (woffs<0) woffs+=DaysPerWk; // we want to go BACK to previous week start | |||
1356 | adjustCursor(es, -woffs); // back to previous start of week (=start of first week covered by recurrence) | |||
1357 | lineardate_t d = es.expansionStartDayOffset+woffs; // days from beginning of first week to beginning day of expansion | |||
1358 | // if we exceed reach of mask in current interval, round up to beginning of next interval | |||
1359 | d = makeMultiple(d,es.interval*DaysPerWk,DaysPerWk-1); // expand into next interval if needed | |||
1360 | adjustCursor(es, d); // now advance cursor to | |||
1361 | if (expansionEnd(es)) goto done; // could by beyond current expanding scope due to interval jump | |||
1362 | } | |||
1363 | // calculate first/next occurrence (if not first occurrence, do not check initially but advance first) | |||
1364 | while(advanceFirst || !( | |||
1365 | es.firstmask & ((uInt64)1<<es.cursorWDay) | |||
1366 | )) { | |||
1367 | advanceFirst = false; // next time we need to check | |||
1368 | // - next day is next candidate | |||
1369 | incCursor(es); // next day | |||
1370 | if (es.cursorWDay==es.weekstart) { | |||
1371 | // a new week has started, we need to skip non-active weeks first | |||
1372 | adjustCursor(es, (es.interval-1)*DaysPerWk); | |||
1373 | } | |||
1374 | } | |||
1375 | // found occurrence | |||
1376 | } // W | |||
1377 | else if (es.freq=='M') { | |||
1378 | if (!es.started) { | |||
1379 | // make sure we have a mask | |||
1380 | if (es.firstmask==0 && es.lastmask==0) { | |||
1381 | if (es.freqmod=='W') { | |||
1382 | es.firstmask = (uInt64)1<<(es.cursorWDay+DaysPerWk*((es.startday-1)/DaysPerWk)); // set start weekday in mask | |||
1383 | } | |||
1384 | else { | |||
1385 | es.firstmask = (uInt64)1<<(es.startday-1); // set start monthday in mask | |||
1386 | } | |||
1387 | } | |||
1388 | // For all monthly repeats: find first month to apply freqmod to | |||
1389 | if (es.expansionStartDayOffset) { | |||
1390 | // does not necessarily start in cursor month | |||
1391 | // - move cursor to where we want to begin earliest | |||
1392 | adjustCursor(es, es.expansionStartDayOffset); | |||
1393 | // - components of the expansion start | |||
1394 | sInt16 y,m; | |||
1395 | lineartime2date(es.cursor,&y,&m,&es.startday); // year, month, day-in-month | |||
1396 | // - how many months since start | |||
1397 | sInt16 numMonths = monthDiff(y,m,es.startyear,es.startmonth); | |||
1398 | sInt16 monthMod = numMonths % es.interval; | |||
1399 | if (monthMod>0) { | |||
1400 | // need to go into subsequent month | |||
1401 | numMonths += es.interval-monthMod; // months to next interval start | |||
1402 | // entire month is after expansion start, so begin with 1st | |||
1403 | es.startday = 1; | |||
1404 | } | |||
1405 | monthAdd(es.startyear,es.startmonth,numMonths); | |||
1406 | // startYear/month/day now on first candidate | |||
1407 | // - calculate linear start | |||
1408 | es.cursor = date2lineartime(es.startyear, es.startmonth, es.startday); | |||
1409 | if (expansionEnd(es)) goto done; // could by beyond current expanding scope due to interval jump | |||
1410 | // - calculate last day in this month | |||
1411 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1412 | // - make sure cursor weekday is correct | |||
1413 | es.cursorWDay = lineartime2weekday(es.cursor); | |||
1414 | } | |||
1415 | } | |||
1416 | // apply freqmod | |||
1417 | if (es.freqmod=='W') { | |||
1418 | // monthly by weekday | |||
1419 | // - calculate first/next occurrence (if not first occurrence, do not check initially but advance first) | |||
1420 | while(true) { | |||
1421 | // calculate which weeks we are in | |||
1422 | sInt16 fwk=(es.startday-1) / DaysPerWk; // start is nth week of the month | |||
1423 | sInt16 lwk=(es.cursorMLen-es.startday) / DaysPerWk; // start is nth-last week of the month | |||
1424 | // check if we found an occurrence | |||
1425 | if (!advanceFirst) { | |||
1426 | if ( | |||
1427 | (es.firstmask & ((uInt64)1<<(es.cursorWDay+DaysPerWk*fwk))) || // nth occurrence of weekday in month | |||
1428 | (es.lastmask & ((uInt64)1<<(es.cursorWDay+DaysPerWk*lwk))) // nth-last occurrence of weekday in month | |||
1429 | ) | |||
1430 | break; | |||
1431 | } | |||
1432 | advanceFirst = false; // next time we need to check | |||
1433 | // - next day-in-month is next candidate | |||
1434 | es.cursorWDay++; if (es.cursorWDay>=DaysPerWk) es.cursorWDay=0; // next day in the week | |||
1435 | es.startday++; | |||
1436 | if (es.startday>es.cursorMLen) { | |||
1437 | // end of month reached | |||
1438 | // - goto next relevant month | |||
1439 | monthAdd(es.startyear,es.startmonth,es.interval); | |||
1440 | // - first day | |||
1441 | es.startday=1; | |||
1442 | // - advance cursor into new month | |||
1443 | es.cursor = date2lineartime(es.startyear, es.startmonth, es.startday); | |||
1444 | // - get length of this month | |||
1445 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1446 | // - make sure cursor weekday is correct | |||
1447 | es.cursorWDay = lineartime2weekday(es.cursor); | |||
1448 | } | |||
1449 | } | |||
1450 | // found occurrence, update cursor | |||
1451 | es.cursor = date2lineartime(es.startyear, es.startmonth, es.startday)+es.starttime; | |||
1452 | } // MW | |||
1453 | else { | |||
1454 | // monthly by monthday list | |||
1455 | // - calculate first/next occurrence (if not first occurrence, do not check initially but advance first) | |||
1456 | while(advanceFirst || !( | |||
1457 | (es.firstmask & ((uInt64)1<<(es.startday-1))) || // nth day in month | |||
1458 | (es.lastmask & ((uInt64)1<<(es.cursorMLen-es.startday))) // nth-last day in month | |||
1459 | )) { | |||
1460 | advanceFirst = false; // next time we need to check | |||
1461 | if (es.singleFMaskBit>=0) { | |||
1462 | // - next candidate is indicated by single mask bit | |||
1463 | if (es.startday<es.singleFMaskBit+1) | |||
1464 | es.startday = es.singleFMaskBit+1; // jump to start day | |||
1465 | else | |||
1466 | es.startday = es.cursorMLen+1; // jump to end of month | |||
1467 | } | |||
1468 | else { | |||
1469 | // - next day-in-month is next candidate | |||
1470 | es.startday++; | |||
1471 | } | |||
1472 | if (es.startday>es.cursorMLen) { | |||
1473 | // end of month reached | |||
1474 | // - goto next relevant month | |||
1475 | monthAdd(es.startyear,es.startmonth,es.interval); | |||
1476 | // - first day | |||
1477 | es.startday=1; | |||
1478 | // - advance cursor into new month | |||
1479 | es.cursor = date2lineartime(es.startyear, es.startmonth, es.startday); | |||
1480 | // - get length of this month | |||
1481 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1482 | } | |||
1483 | } | |||
1484 | // found occurrence, update cursor | |||
1485 | es.cursor = date2lineartime(es.startyear, es.startmonth, es.startday)+es.starttime; | |||
1486 | } // MD | |||
1487 | } // M | |||
1488 | else if (es.freq=='Y') { | |||
1489 | if (!es.started) { | |||
1490 | // Make sure we have a mask or cancel freqmod entirely | |||
1491 | if (es.firstmask==0 && es.lastmask==0) { | |||
1492 | // no mask at all, only startmonth counts anyway -> revert to simple direct calculation (cancel freqmod) | |||
1493 | es.freqmod = 0; | |||
1494 | } | |||
1495 | else if (es.singleFMaskBit>=0 && es.singleFMaskBit+1==es.startmonth) { | |||
1496 | // mask with single bit on start date month -> can revert to simple diect calculation (cancel freqmod) | |||
1497 | es.freqmod = 0; // no month list parsing needed | |||
1498 | } | |||
1499 | // For all yearly repeats: find first year to apply freqmod to | |||
1500 | if (es.expansionStartDayOffset) { | |||
1501 | // does not necessarily start in cursor year | |||
1502 | // - move cursor to where we want to begin earliest | |||
1503 | adjustCursor(es, es.expansionStartDayOffset); | |||
1504 | // - year of the expansion start | |||
1505 | sInt16 y,m,d; | |||
1506 | lineartime2date(es.cursor,&y,&m,&d); // year, month, day-in-month | |||
1507 | // - make sure cursor can be a candidate (i.e. is not before start) | |||
1508 | if (es.freqmod=='M') { | |||
1509 | if (d>es.startday) | |||
1510 | monthAdd(y, m, 1); // no occurrence possible in this month, next candidate is in next month | |||
1511 | } | |||
1512 | else { | |||
1513 | if (m>es.startmonth || (m==es.startmonth && d>es.startday)) | |||
1514 | y++; // no occurrence possible this year, next candidate is in next year | |||
1515 | } | |||
1516 | // - make sure we are in a year at the beginning of the interval | |||
1517 | sInt16 numYears = y-es.startyear; | |||
1518 | sInt16 yearMod = numYears % es.interval; | |||
1519 | if (yearMod>0) { | |||
1520 | // need to go into subsequent year | |||
1521 | numYears += es.interval-yearMod; // months to next interval start | |||
1522 | m = 1; // start with Jan again (only used below when checking monthlist i.e. freqmod==M) | |||
1523 | } | |||
1524 | es.startyear += numYears; | |||
1525 | if (es.freqmod=='M') { | |||
1526 | // month-by-month checking | |||
1527 | es.startmonth = m; | |||
1528 | } | |||
1529 | // startYear/month/day now on first candidate | |||
1530 | // - calculate linear start | |||
1531 | es.cursor = date2lineartime(es.startyear, es.startmonth, 1)+es.starttime; | |||
1532 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1533 | adjustCursor(es, es.startday-1); | |||
1534 | if (expansionEnd(es)) goto done; // could by beyond current expanding scope due to interval jump | |||
1535 | // - calculate last day in this month | |||
1536 | } | |||
1537 | } | |||
1538 | // apply freqmod | |||
1539 | if (es.freqmod=='M') { | |||
1540 | // yearly by month list (and we really have a month list, not only a single month bit) | |||
1541 | // - calculate first/next occurrence (if not first occurrence, do not check initially but advance first) | |||
1542 | int yearsscanned = 0; | |||
1543 | do { | |||
1544 | while(advanceFirst || !( | |||
1545 | (es.firstmask & ((uInt64)1<<(es.startmonth-1))) || // nth month | |||
1546 | (es.lastmask & ((uInt64)1<<(12-es.startmonth))) // nth-last month | |||
1547 | )) { | |||
1548 | advanceFirst = false; // next time we need to check | |||
1549 | // - next month is next candidate | |||
1550 | es.startmonth++; | |||
1551 | if (es.startmonth>12) { | |||
1552 | // end of year reached | |||
1553 | // - goto next relevant year | |||
1554 | es.startyear += es.interval; | |||
1555 | yearsscanned ++; | |||
1556 | // - first month again | |||
1557 | es.startmonth = 1; | |||
1558 | } | |||
1559 | } | |||
1560 | // found possible occurrence (could be a day that does not exist in the month), update cursor | |||
1561 | es.cursor = date2lineartime(es.startyear, es.startmonth, 1)+es.starttime; | |||
1562 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1563 | adjustCursor(es, es.startday-1); | |||
1564 | // repeat until day actually exists | |||
1565 | if (es.startday>es.cursorMLen) { | |||
1566 | // disable checking for match again | |||
1567 | advanceFirst = true; | |||
1568 | } | |||
1569 | } while(advanceFirst && yearsscanned<5); | |||
1570 | // found occurrence | |||
1571 | } // YM | |||
1572 | else { | |||
1573 | // yearly on same date | |||
1574 | while (advanceFirst || es.startday>es.cursorMLen ) { | |||
1575 | advanceFirst = false; // next time we need to check | |||
1576 | // just calculate next candiate | |||
1577 | es.startyear += es.interval; | |||
1578 | // update cursor and calculate | |||
1579 | es.cursor = date2lineartime(es.startyear, es.startmonth, 1)+es.starttime; | |||
1580 | es.cursorMLen = getMonthDays(lineartime2dateonly(es.cursor)); | |||
1581 | adjustCursor(es, es.startday-1); | |||
1582 | } | |||
1583 | // found occurrence | |||
1584 | } // Yx | |||
1585 | } // Y | |||
1586 | else { | |||
1587 | // unknown = nonexpandable recurrence | |||
1588 | // - return start date as first occurrence to make sure it does not get invisible | |||
1589 | if (es.started) { | |||
1590 | // subsequent occurrence requested: not available | |||
1591 | es.cursor = noLinearTime; | |||
1592 | } | |||
1593 | } | |||
1594 | // stop expansion if end of expansion period reached | |||
1595 | expansionEnd(es); | |||
1596 | done: | |||
1597 | // done | |||
1598 | es.started = true; | |||
1599 | return es.cursor; | |||
1600 | } // getNthNextOccurrence | |||
1601 | ||||
1602 | ||||
1603 | ||||
1604 | ||||
1605 | ||||
1606 | ||||
1607 | ||||
1608 | ||||
1609 | /// @brief calculate number of recurrences from specified end date of RRULE | |||
1610 | /// @return true if count could be calculated, false otherwise | |||
1611 | /// @param[in] dtstart, until : must be in the correct context to evaluate weekday rules | |||
1612 | /// @param[in] countsoccurrences : if true, counting occurrences (rather than intervals) | |||
1613 | /// @note returns until=noLinearTime for endless repeat (count=0) | |||
1614 | bool countFromEndDate( | |||
1615 | sInt16 &cnt, bool countsoccurrences, | |||
1616 | lineartime_t dtstart, | |||
1617 | char freq, char freqmod, | |||
1618 | sInt16 interval, | |||
1619 | fieldinteger_t firstmask,fieldinteger_t lastmask, | |||
1620 | lineartime_t until, | |||
1621 | TDebugLogger *aLogP | |||
1622 | ) | |||
1623 | { | |||
1624 | if (dtstart==noLinearTime || interval<1) return false; // no start date or interval, cannot calc count | |||
1625 | if (until==noLinearTime) { | |||
1626 | // endless repeat | |||
1627 | cnt=0; | |||
1628 | return true; | |||
1629 | } | |||
1630 | // iterate until end date is reached | |||
1631 | cnt=1; | |||
1632 | lineartime_t occurrence=noLinearTime; | |||
1633 | // break after 500 recurrences | |||
1634 | while (cnt<500) { | |||
1635 | if (!endDateFromCount( | |||
1636 | occurrence, | |||
1637 | dtstart, | |||
1638 | freq,freqmod, | |||
1639 | interval, | |||
1640 | firstmask,lastmask, | |||
1641 | cnt, | |||
1642 | countsoccurrences, | |||
1643 | aLogP | |||
1644 | )) | |||
1645 | return false; // error, cannot calc end date | |||
1646 | if (occurrence>until) { | |||
1647 | // no more occurrences | |||
1648 | cnt--; | |||
1649 | if (cnt==0) return false; // if not endless, but no occurrence found in range -> not repeating | |||
1650 | return true; // return count | |||
1651 | } | |||
1652 | // next occurrence | |||
1653 | cnt++; | |||
1654 | } | |||
1655 | // 500 and more recurrences count as endless | |||
1656 | cnt=0; | |||
1657 | return true; | |||
1658 | } // countFromEndDate | |||
1659 | ||||
1660 | ||||
1661 | ||||
1662 | // Converts vCalendar 2.0 RRULE string into internal recurrence representation | |||
1663 | bool RRULE2toInternal( | |||
1664 | const char *aText, // RRULE string to be parsed | |||
1665 | lineartime_t dtstart, // reference date for parsing RRULE | |||
1666 | timecontext_t startcontext, // context of reference date | |||
1667 | char &freq, | |||
1668 | char &freqmod, | |||
1669 | sInt16 &interval, | |||
1670 | fieldinteger_t &firstmask, | |||
1671 | fieldinteger_t &lastmask, | |||
1672 | lineartime_t &until, | |||
1673 | timecontext_t &untilcontext, | |||
1674 | TDebugLogger *aLogP, | |||
1675 | lineartime_t *aNewStartP | |||
1676 | ) | |||
1677 | { | |||
1678 | #ifdef SYDEBUG2 | |||
1679 | string abc; | |||
1680 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("RRULE2toInternal(): start analyzing '%s'", aText)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("RRULE2toInternal(): start analyzing '%s'" , aText); }; | |||
1681 | #endif | |||
1682 | ||||
1683 | string temp; | |||
1684 | const char *p; | |||
1685 | char s [50]; | |||
1686 | sInt16 startwday; | |||
1687 | sInt16 startyear,startmonth,startday; | |||
1688 | ||||
1689 | string::size_type endIndex = 0; | |||
1690 | string::size_type startIndex = 0; | |||
1691 | uInt16 cnt = 0; | |||
1692 | size_t pos; | |||
1693 | bool calculateEndDate = false; // indicator that end date calc is required as last step | |||
1694 | bool calculateFirstOccurrence = false; // indicator that aNewStartP should be adjusted to show the first occurrence | |||
1695 | string key,value,byday,bymonthday,bymonth; | |||
1696 | ||||
1697 | // get elements of start point | |||
1698 | startwday=lineartime2weekday(dtstart); // get starting weekday | |||
1699 | lineartime2date(dtstart,&startyear,&startmonth,&startday); // year, month, day-in-month | |||
1700 | until= noLinearTime; // initialize end point, if not available | |||
1701 | ||||
1702 | // do the conversion here | |||
1703 | p=aText; | |||
1704 | int start = 0; | |||
1705 | // get freq | |||
1706 | if (!getNextDirective(temp, p, start)) { | |||
| ||||
1707 | // failed | |||
1708 | goto incompat; | |||
1709 | } | |||
1710 | // analyze freq | |||
1711 | if (temp == "FREQ=YEARLY") { | |||
1712 | freq='Y'; | |||
1713 | } | |||
1714 | else if (temp == "FREQ=MONTHLY") { | |||
1715 | freq='M'; | |||
1716 | } | |||
1717 | else if (temp == "FREQ=WEEKLY") { | |||
1718 | freq='W'; | |||
1719 | } | |||
1720 | else if (temp == "FREQ=DAILY") { | |||
1721 | freq='D'; | |||
1722 | } | |||
1723 | else { | |||
1724 | goto incompat; | |||
1725 | } | |||
1726 | ||||
1727 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- found frequency %s which maps to freq %c", temp.c_str(), freq)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- found frequency %s which maps to freq %c" , temp.c_str(), freq); }; | |||
1728 | ||||
1729 | // init vars | |||
1730 | cnt = 0; | |||
1731 | ||||
1732 | // set interval to 1 | |||
1733 | interval = 1; | |||
1734 | freqmod = ' '; | |||
1735 | firstmask = 0; | |||
1736 | lastmask = 0; | |||
1737 | // get next directives | |||
1738 | while (getNextDirective(temp, p, start)) { | |||
1739 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- found next directive %s", temp.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- found next directive %s" , temp.c_str()); }; | |||
1740 | // split | |||
1741 | pos = temp.find("="); | |||
1742 | if (pos == string::npos || pos == 0 || pos == (temp.length() - 1)) { | |||
1743 | goto incompat; | |||
1744 | } | |||
1745 | key = temp.substr(0, pos); | |||
1746 | value = temp.substr(pos + 1, temp.length() - 1); | |||
1747 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted key/value %s/%s", key.c_str(), value.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted key/value %s/%s" , key.c_str(), value.c_str()); }; | |||
1748 | // check for until | |||
1749 | if (key == "UNTIL") { | |||
1750 | if (ISO8601StrToTimestamp(value.c_str(), until, untilcontext)==0) | |||
1751 | goto incompat; // invalid end date | |||
1752 | calculateEndDate = false; | |||
1753 | } | |||
1754 | // check for count | |||
1755 | else if (key == "COUNT") { | |||
1756 | // convert count | |||
1757 | for (int i = 0, ii = value.length(); i < ii; ++i) { | |||
1758 | if (isdigit(value[i])) { | |||
1759 | cnt = cnt * 10 + ((value[i]) - '0'); | |||
1760 | } | |||
1761 | } | |||
1762 | // check if no count or one only | |||
1763 | if (cnt <= 1) | |||
1764 | goto norep; // no repetition | |||
1765 | // we need to recalculate the enddate from count | |||
1766 | calculateEndDate = true; | |||
1767 | } | |||
1768 | // check for interval | |||
1769 | else if (key == "INTERVAL") { | |||
1770 | // convert interval | |||
1771 | interval = 0; | |||
1772 | for (int i = 0, ii = value.length(); i < ii; ++i) { | |||
1773 | if (isdigit(value[i])) { | |||
1774 | interval = interval * 10 + ((value[i]) - '0'); | |||
1775 | } | |||
1776 | } | |||
1777 | if (interval == 0) | |||
1778 | goto incompat; // invalid interval | |||
1779 | } | |||
1780 | // just copy all supported BYxxx rules into vars | |||
1781 | else if (key == "BYDAY") | |||
1782 | byday = value; | |||
1783 | else if (key == "BYMONTHDAY") | |||
1784 | bymonthday = value; | |||
1785 | else if (key == "BYMONTH") | |||
1786 | bymonth = value; | |||
1787 | // ignore week start | |||
1788 | else if (key == "WKST") { | |||
1789 | // nop | |||
1790 | } | |||
1791 | else | |||
1792 | goto incompat; // unknown directive | |||
1793 | } // while | |||
1794 | ||||
1795 | #ifdef SYDEBUG2 | |||
1796 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- dtstart weekday is %i", startwday)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- dtstart weekday is %i" , startwday); }; | |||
1797 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted interval %u", interval)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted interval %u" , interval); }; | |||
1798 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted interval (2nd time) %u", interval)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted interval (2nd time) %u" , interval); }; | |||
1799 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted count %u", cnt)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted count %u" , cnt); }; | |||
1800 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- end date calc required? %s", calculateEndDate ? "true" : "false")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- end date calc required? %s" , calculateEndDate ? "true" : "false"); }; | |||
1801 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted byday %s", byday.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted byday %s" , byday.c_str()); }; | |||
1802 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted bymonthday %s", bymonthday.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted bymonthday %s" , bymonthday.c_str()); }; | |||
1803 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted bymonth %s", bymonth.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted bymonth %s" , bymonth.c_str()); }; | |||
1804 | TimestampToISO8601Str(abc,until,startcontext,false,false); | |||
1805 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- extracted until %s", abc.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- extracted until %s" , abc.c_str()); }; | |||
1806 | #endif | |||
1807 | ||||
1808 | // Generally, we only support one BYxxx, not combinations. However, FREQ=YEARLY + BYMONTH + BYDAY is common | |||
1809 | // to express start and end times of DST in VTIMEZONEs, so we convert these into equivalent FREQ=MONTHLY | |||
1810 | if (freq=='Y' && !byday.empty() && !bymonth.empty() && bymonth.find(",")==string::npos) { | |||
1811 | // yearly by (single!) month and by day | |||
1812 | // - get month as indicated by BYMONTH | |||
1813 | sInt16 newStartMonth = 0; | |||
1814 | if (StrToShort(bymonth.c_str(), newStartMonth)>0 && newStartMonth) { | |||
1815 | // - check if BYMONTH is just redundant and references same month as start date | |||
1816 | bool convertToMonthly = startmonth>0 && startmonth==newStartMonth; | |||
1817 | if (!convertToMonthly && aNewStartP) { | |||
1818 | // not redundant, but we can return a new start date | |||
1819 | convertToMonthly = true; | |||
1820 | // calculate the new start date | |||
1821 | startmonth = newStartMonth; | |||
1822 | dtstart = date2lineartime(startyear, startmonth, startday) + lineartime2timeonly(dtstart); | |||
1823 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- moved recurrence origin to %04hd-%02hd-%02hd",startyear, startmonth, startday)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- moved recurrence origin to %04hd-%02hd-%02hd" ,startyear, startmonth, startday); }; | |||
1824 | // pass it back to caller | |||
1825 | calculateFirstOccurrence = true; // need to fine tune at the end | |||
1826 | *aNewStartP = dtstart; | |||
1827 | } | |||
1828 | if (convertToMonthly) { | |||
1829 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- converted FREQ=YEARLY BYMONTH into MONTHLY")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- converted FREQ=YEARLY BYMONTH into MONTHLY" ); }; | |||
1830 | freq = 'M'; // convert from YEARLY to MONTHLY | |||
1831 | interval = 12*interval; // but with 12 months interval -> equivalent | |||
1832 | bymonth.erase(); // forget BYMONTH clause | |||
1833 | } | |||
1834 | } | |||
1835 | } // if FREQ=YEARLY + BYMONTH + BYDAY | |||
1836 | // check freq | |||
1837 | endIndex = 0; | |||
1838 | startIndex = 0; | |||
1839 | switch (freq) { | |||
1840 | case 'D' : | |||
1841 | // make weekly if byday and nothing else is set | |||
1842 | if (!(byday == "") && bymonth == "" && bymonthday == "") { | |||
1843 | freq='W'; | |||
1844 | freqmod='W'; | |||
1845 | // search separator ',' | |||
1846 | while ((endIndex = byday.find(",", startIndex)) != string::npos) { | |||
1847 | // set masks for the specific day | |||
1848 | if (!setWeekday(byday, firstmask, lastmask, startIndex, endIndex, false)) | |||
1849 | goto incompat; | |||
1850 | // set new start | |||
1851 | startIndex = endIndex + 1; | |||
1852 | if (startIndex >= byday.length()) | |||
1853 | break; | |||
1854 | } | |||
1855 | // check if anything is behind endindex | |||
1856 | if (endIndex == string::npos && startIndex < byday.length()) { | |||
1857 | endIndex = byday.length(); | |||
1858 | if (!setWeekday(byday, firstmask, lastmask, startIndex, endIndex, false)) | |||
1859 | goto incompat; | |||
1860 | } | |||
1861 | } | |||
1862 | else { | |||
1863 | // we don't support byday and anything else at the same time | |||
1864 | if (!byday.empty()) | |||
1865 | goto incompat; | |||
1866 | // ok, no mod | |||
1867 | freqmod = ' '; | |||
1868 | } | |||
1869 | break; | |||
1870 | case 'W' : | |||
1871 | // we don't support month or monthday within weekly | |||
1872 | if (!bymonth.empty() || !bymonthday.empty()) | |||
1873 | goto incompat; | |||
1874 | // set weekly and days | |||
1875 | freqmod='W'; | |||
1876 | if (!byday.empty()) { | |||
1877 | // search separator ',' | |||
1878 | while ((endIndex = byday.find(",", startIndex)) != string::npos) { | |||
1879 | // set masks for the specific day | |||
1880 | if (!setWeekday(byday, firstmask, lastmask, startIndex, endIndex, false)) | |||
1881 | goto incompat; | |||
1882 | // set new start | |||
1883 | startIndex = endIndex + 1; | |||
1884 | if (startIndex >= byday.length()) | |||
1885 | break; | |||
1886 | } | |||
1887 | // check if anything is behind endindex | |||
1888 | if (endIndex == string::npos && startIndex < byday.length()) { | |||
1889 | endIndex = byday.length(); | |||
1890 | if (!setWeekday(byday, firstmask, lastmask, startIndex, endIndex, false)) | |||
1891 | goto incompat; | |||
1892 | } | |||
1893 | } | |||
1894 | break; | |||
1895 | case 'M' : | |||
1896 | // we don't support by month in monthly | |||
1897 | if (!bymonth.empty()) | |||
1898 | goto incompat; | |||
1899 | // we don't support byday and bymonthday at the same time | |||
1900 | if (!byday.empty() && !bymonthday.empty()) | |||
1901 | goto incompat; | |||
1902 | // check if bymonthday | |||
1903 | if (!bymonthday.empty()) { | |||
1904 | freqmod = 'D'; | |||
1905 | // search separator ',' | |||
1906 | while ((endIndex = bymonthday.find(",", startIndex)) != string::npos) { | |||
1907 | // set masks for the specific day | |||
1908 | if (!setMonthDay(bymonthday, firstmask, lastmask, startIndex, endIndex)) | |||
1909 | goto incompat; | |||
1910 | // set new start | |||
1911 | startIndex = endIndex + 1; | |||
1912 | if (startIndex >= bymonthday.length()) | |||
1913 | break; | |||
1914 | } | |||
1915 | // check if anything is behind endindex | |||
1916 | if (endIndex == string::npos && startIndex < bymonthday.length()) { | |||
1917 | endIndex = bymonthday.length(); | |||
1918 | if (!setMonthDay(bymonthday, firstmask, lastmask, startIndex, endIndex)) | |||
1919 | goto incompat; | |||
1920 | } | |||
1921 | } | |||
1922 | // or if by weekday | |||
1923 | else if (!byday.empty()) { | |||
1924 | freqmod = 'W'; | |||
1925 | // search separator ',' | |||
1926 | while ((endIndex = byday.find(",", startIndex)) != string::npos) { | |||
1927 | // set masks for the specific day | |||
1928 | if (!setWeekday(byday, firstmask, lastmask, startIndex, endIndex, true)) | |||
1929 | goto incompat; | |||
1930 | // set new start | |||
1931 | startIndex = endIndex + 1; | |||
1932 | if (startIndex >= byday.length()) | |||
1933 | break; | |||
1934 | } | |||
1935 | // check if anything is behind endindex | |||
1936 | if (endIndex == string::npos && startIndex < byday.length()) { | |||
1937 | endIndex = byday.length(); | |||
1938 | if (!setWeekday(byday, firstmask, lastmask, startIndex, endIndex, true)) | |||
1939 | goto incompat; | |||
1940 | } | |||
1941 | } | |||
1942 | else { | |||
1943 | // fine, no mod | |||
1944 | freqmod = ' '; | |||
1945 | } | |||
1946 | break; | |||
1947 | case 'Y' : | |||
1948 | if ( | |||
1949 | byday == "" || | |||
1950 | (byday.length() == 2 && byday[0] == RRULE_weekdays[startwday][0] && | |||
1951 | byday[1] == RRULE_weekdays[startwday][1]) | |||
1952 | ) { | |||
1953 | temp.erase(); | |||
1954 | sprintf(s, "%hd", startday); | |||
1955 | temp.append(s); | |||
1956 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- bymonthday checking %s against %s", temp.c_str(), bymonthday.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- bymonthday checking %s against %s" , temp.c_str(), bymonthday.c_str()); }; | |||
1957 | if (bymonthday == "" || bymonthday == temp) { | |||
1958 | temp.erase(); | |||
1959 | sprintf(s, "%hd", startmonth); | |||
1960 | temp.append(s); | |||
1961 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- bymonth checking %s against %s", temp.c_str(), bymonth.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- bymonth checking %s against %s" , temp.c_str(), bymonth.c_str()); }; | |||
1962 | if (bymonth == "" || bymonth == temp) { | |||
1963 | // this should usually be ' ' but the vcard conversion has a bug and requires 'M' | |||
1964 | freqmod = 'M'; | |||
1965 | lastmask = 0; | |||
1966 | firstmask = 0; | |||
1967 | } | |||
1968 | else | |||
1969 | goto incompat; | |||
1970 | } | |||
1971 | else | |||
1972 | goto incompat; | |||
1973 | } | |||
1974 | else | |||
1975 | goto incompat; | |||
1976 | break; | |||
1977 | default : | |||
1978 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- strange self-set freq %c, rule was %s", freq, aText)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- strange self-set freq %c, rule was %s" , freq, aText); }; | |||
1979 | break; | |||
1980 | } // switch | |||
1981 | ||||
1982 | // calc end date, assumption/make sure: cnt > 1 | |||
1983 | if (calculateEndDate) { | |||
1984 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- calculating end date now")){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- calculating end date now" ); }; | |||
1985 | if (!endDateFromCount(until,dtstart,freq,freqmod,interval,firstmask,lastmask,cnt,true,aLogP)) | |||
1986 | goto norep; | |||
1987 | untilcontext = startcontext; // until is in same context as start | |||
1988 | } | |||
1989 | // calculate exact date of first occurrence | |||
1990 | if (calculateFirstOccurrence && aNewStartP) { | |||
1991 | TRRuleExpandStatus es; | |||
1992 | initRRuleExpansion(es, dtstart, freq, freqmod, interval, firstmask, lastmask, dtstart, noLinearTime); | |||
1993 | dtstart = getNextOccurrence(es); | |||
1994 | if (LOGDEBUGTEST(aLogP,DBG_PARSE+DBG_EXOTIC)((aLogP) && (((0x00000200 +0x80000000) & (aLogP)-> getMask()) == (0x00000200 +0x80000000)))) { | |||
1995 | sInt16 ny,nm,nd; | |||
1996 | lineartime2date(dtstart,&ny,&nm,&nd); | |||
1997 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- adjusted first occurrence date to %04hd-%02hd-%02hd",ny, nm, nd)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- adjusted first occurrence date to %04hd-%02hd-%02hd" ,ny, nm, nd); }; | |||
1998 | } | |||
1999 | *aNewStartP = dtstart; | |||
2000 | } | |||
2001 | // parsed ok, now store it | |||
2002 | goto store; | |||
2003 | norep: | |||
2004 | // no repetition (but no parse error generated) | |||
2005 | freq='0'; // frequency = none | |||
2006 | freqmod=' '; // frequency modifier | |||
2007 | interval=0; // interval | |||
2008 | firstmask=0; // day mask counted from the first day of the period | |||
2009 | lastmask=0; // day mask counted from the last day of the period | |||
2010 | until= noLinearTime; // last day | |||
2011 | store: | |||
2012 | ||||
2013 | #ifdef SYDEBUG2 | |||
2014 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("- leaving with freq %c, freqmod %c, interval %hd, firstmask %ld, lastmask %ld", freq, freqmod, interval, (long)firstmask, (long)lastmask)){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("- leaving with freq %c, freqmod %c, interval %hd, firstmask %ld, lastmask %ld" , freq, freqmod, interval, (long)firstmask, (long)lastmask); }; | |||
2015 | TimestampToISO8601Str(abc,until,untilcontext,false,false); | |||
2016 | LOGDEBUGPRINTFX(aLogP,DBG_PARSE+DBG_EXOTIC,("RRULE2toInternal(): extracted until '%s'", abc.c_str())){ if ((aLogP) && ((0x00000200 +0x80000000) & (aLogP )->getMask()) == (0x00000200 +0x80000000)) (aLogP)->setNextMask (0x00000200 +0x80000000).DebugPrintfLastMask ("RRULE2toInternal(): extracted until '%s'" , abc.c_str()); }; | |||
2017 | #endif | |||
2018 | ||||
2019 | return true; // ok | |||
2020 | incompat: | |||
2021 | // incompatible, value cannot be parsed usefully | |||
2022 | return false; // no value generated | |||
2023 | } // RRULE2toInternal | |||
2024 | ||||
2025 | ||||
2026 | ||||
2027 | // appends the firstmask and lastmask as numbers to the string. | |||
2028 | // all values are increased by one prior to adding them to string. | |||
2029 | void appendMaskAsNumbers( | |||
2030 | cAppCharP aPrefix, // if list is not empty, this will be appended in front of the list | |||
2031 | string &aString, // receives list string | |||
2032 | fieldinteger_t firstmask, | |||
2033 | fieldinteger_t lastmask | |||
2034 | ) | |||
2035 | { | |||
2036 | fieldinteger_t m = firstmask; | |||
2037 | int i,k; | |||
2038 | for (i=0; i<2; i++) { | |||
2039 | // - show day numbers | |||
2040 | for (k=0; k<32; k++) { | |||
2041 | if (m & ((uInt64)1<<k)) { | |||
2042 | aString+=aPrefix; | |||
2043 | aPrefix=","; // prefix for further elements is now colon. | |||
2044 | if (i>0) aString+='-'; | |||
2045 | StringObjAppendPrintf(aString,"%d",k+1); | |||
2046 | } | |||
2047 | } | |||
2048 | // - switch to those that are relative to the end of the month / year | |||
2049 | m = lastmask; | |||
2050 | } | |||
2051 | } // appendMaskAsNumbers | |||
2052 | ||||
2053 | ||||
2054 | // returns the next directive for the ical format | |||
2055 | bool getNextDirective( | |||
2056 | string &aString, | |||
2057 | const char *aText, | |||
2058 | int &aStart | |||
2059 | ) | |||
2060 | { | |||
2061 | // check | |||
2062 | if (*aText == 0) { | |||
2063 | return false; | |||
2064 | } | |||
2065 | ||||
2066 | char c; | |||
2067 | // check start | |||
2068 | if (aStart > 0) { | |||
2069 | // skip to start | |||
2070 | for (int i = 0; (i < aStart) && ((c = *aText) != 0); ++i) aText++; | |||
2071 | } | |||
2072 | else { | |||
2073 | // skip all spaces | |||
2074 | while ((c = *aText) == ' ') { | |||
2075 | aText++; | |||
2076 | } | |||
2077 | } | |||
2078 | // erase string, set counter | |||
2079 | aString.erase(); | |||
2080 | uInt16 counter = 0; | |||
2081 | // append text | |||
2082 | c = *aText++; aStart++; | |||
2083 | while (c != 0 && c != ';') { | |||
2084 | aString.append(1, c); | |||
2085 | ++counter; | |||
2086 | c = *aText++; aStart++; | |||
2087 | } | |||
2088 | // empty? | |||
2089 | if (counter == 0) | |||
2090 | return false; | |||
2091 | // remove trailing whitespace | |||
2092 | sInt16 i = counter - 1; | |||
2093 | while (i >= 0 && aString[i] == ' ') { | |||
2094 | aString.erase(i, 1); | |||
2095 | --i; | |||
2096 | } | |||
2097 | // check length | |||
2098 | if (i == -1) | |||
2099 | return false; | |||
2100 | // fine | |||
2101 | return true; | |||
2102 | } // getNextDirective | |||
2103 | ||||
2104 | ||||
2105 | ||||
2106 | // maps the byday rule into the masks | |||
2107 | bool setWeekday( | |||
2108 | const string &byday, | |||
2109 | fieldinteger_t &firstmask, | |||
2110 | fieldinteger_t &lastmask, | |||
2111 | string::size_type &startIndex, | |||
2112 | const string::size_type &endIndex, | |||
2113 | bool allowSpecial | |||
2114 | ) | |||
2115 | { | |||
2116 | // remove leading spaces | |||
2117 | while (byday[startIndex] == ' ' && startIndex < endIndex) { | |||
2118 | ++startIndex; | |||
2119 | } | |||
2120 | // check if digit or sign | |||
2121 | if (isdigit(byday[startIndex]) || byday[startIndex] == '+' || byday[startIndex] == '-') { | |||
2122 | // check if numbers before week days are allowed | |||
2123 | if (!allowSpecial) { | |||
2124 | return false; | |||
2125 | } | |||
2126 | // special treatment | |||
2127 | return setSpecialWeekday(byday, firstmask, lastmask, startIndex, endIndex); | |||
2128 | } | |||
2129 | // get index of weekday array | |||
2130 | sInt16 weekdayIndex = getWeekdayIndex(byday, startIndex); | |||
2131 | if (weekdayIndex == -1) { | |||
2132 | return false; | |||
2133 | } | |||
2134 | // put into mask | |||
2135 | if (!allowSpecial) { | |||
2136 | firstmask |= ((uInt64)1<<weekdayIndex); | |||
2137 | return true; | |||
2138 | } | |||
2139 | for (int i= 0; i<WeeksOfMonth; i++) { // do it for all weeks of the month | |||
2140 | firstmask |= ((uInt64)1<<weekdayIndex); | |||
2141 | weekdayIndex+= DaysOfWeek; | |||
2142 | } // for | |||
2143 | return true; | |||
2144 | } // setWeekday | |||
2145 | ||||
2146 | ||||
2147 | ||||
2148 | // maps a special weekday (+/- int WEEKDAY) into the masks | |||
2149 | bool setSpecialWeekday( | |||
2150 | const string &byday, | |||
2151 | fieldinteger_t &firstmask, | |||
2152 | fieldinteger_t &lastmask, | |||
2153 | string::size_type &startIndex, | |||
2154 | const string::size_type &endIndex | |||
2155 | ) | |||
2156 | { | |||
2157 | // indicator for negative values | |||
2158 | bool isNegative; | |||
2159 | // the value | |||
2160 | sInt16 val = 0; | |||
2161 | ||||
2162 | // check sign | |||
2163 | if (byday[startIndex] == '-') { | |||
2164 | isNegative = true; | |||
2165 | ++startIndex; | |||
2166 | } | |||
2167 | else if (byday[startIndex] == '+') { | |||
2168 | isNegative = false; | |||
2169 | ++startIndex; | |||
2170 | } | |||
2171 | else { | |||
2172 | isNegative = false; | |||
2173 | } | |||
2174 | // convert to number | |||
2175 | while (isdigit(byday[startIndex])) { | |||
2176 | val = val * 10 + ((byday[startIndex++]) - '0'); | |||
2177 | } | |||
2178 | // remove leading spaces | |||
2179 | while (byday[startIndex] == ' ' && startIndex < endIndex) { | |||
2180 | ++startIndex; | |||
2181 | } | |||
2182 | // make sure there's enough space | |||
2183 | if (startIndex >= endIndex - 1) { | |||
2184 | return false; | |||
2185 | } | |||
2186 | // get index for weekday | |||
2187 | sInt16 index = getWeekdayIndex(byday, startIndex); | |||
2188 | if (index == -1) { | |||
2189 | return false; | |||
2190 | } | |||
2191 | // put into mask | |||
2192 | if (isNegative) { | |||
2193 | lastmask |= (((uInt64)1<<index)<<((val - 1) * DaysOfWeek)); | |||
2194 | } | |||
2195 | else { | |||
2196 | firstmask |= (((uInt64)1<<index)<<((val - 1) * DaysOfWeek)); | |||
2197 | } | |||
2198 | return true; | |||
2199 | } // setSpecialWeekday | |||
2200 | ||||
2201 | ||||
2202 | // returns the index within the rrule weekday array for the next | |||
2203 | // two chars of the supplied string starting at startIndex | |||
2204 | sInt16 getWeekdayIndex( | |||
2205 | const string &byday, | |||
2206 | sInt16 startIndex | |||
2207 | ) | |||
2208 | { | |||
2209 | // search | |||
2210 | for (sInt16 i = 0; i < DaysOfWeek; ++i) | |||
2211 | { | |||
2212 | if (RRULE_weekdays[i][0] == byday[startIndex] && RRULE_weekdays[i][1] == byday[startIndex + 1]) | |||
2213 | return i; | |||
2214 | } | |||
2215 | // not found | |||
2216 | return -1; | |||
2217 | } // getWeekdayIndex | |||
2218 | ||||
2219 | ||||
2220 | // set a day in month | |||
2221 | bool setMonthDay( | |||
2222 | const string &bymonthday, | |||
2223 | fieldinteger_t &firstmask, | |||
2224 | fieldinteger_t &lastmask, | |||
2225 | string::size_type &startIndex, | |||
2226 | const string::size_type &endIndex | |||
2227 | ) | |||
2228 | { | |||
2229 | // indicator for negative values | |||
2230 | bool isNegative; | |||
2231 | // the value | |||
2232 | sInt16 val=0; | |||
2233 | ||||
2234 | // check sign | |||
2235 | if (bymonthday[startIndex] == '-') { | |||
2236 | isNegative = true; | |||
2237 | ++startIndex; | |||
2238 | } | |||
2239 | else if (bymonthday[startIndex] == '+') { | |||
2240 | isNegative = false; | |||
2241 | ++startIndex; | |||
2242 | } | |||
2243 | else { | |||
2244 | isNegative = false; | |||
2245 | } | |||
2246 | // convert to number | |||
2247 | for (string::size_type i = startIndex; i < endIndex; ++i) { | |||
2248 | if (isdigit(bymonthday[i])) { | |||
2249 | val = val * 10 + ((bymonthday[i]) - '0'); | |||
2250 | } | |||
2251 | else { | |||
2252 | return false; | |||
2253 | } | |||
2254 | } | |||
2255 | // put into mask | |||
2256 | if (isNegative) { | |||
2257 | lastmask |= ((uInt64)1<<(val - 1)); | |||
2258 | } | |||
2259 | else { | |||
2260 | firstmask |= ((uInt64)1<<(val - 1)); | |||
| ||||
2261 | } | |||
2262 | return true; | |||
2263 | } // setMonthDay | |||
2264 | ||||
2265 | } // namespace sysync | |||
2266 | ||||
2267 | /* eof */ |