1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Evolution calendar recurrence rule functions
4 *
5 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
6 *
7 * This library is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU Lesser General Public License as published by
9 * the Free Software Foundation.
10 *
11 * This library is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
14 * for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * Authors: Damon Chaplin <damon@ximian.com>
20 */
21
22 #include "evolution-data-server-config.h"
23
24 #include <stdlib.h>
25 #include <string.h>
26 #include <glib/gi18n-lib.h>
27
28 #include "e-cal-recur.h"
29 #include "e-cal-time-util.h"
30 #include "e-cal-client.h"
31
32 static gint
e_timetype_compare(gconstpointer aa,gconstpointer bb)33 e_timetype_compare (gconstpointer aa,
34 gconstpointer bb)
35 {
36 ICalTime *tta = (ICalTime *) aa;
37 ICalTime *ttb = (ICalTime *) bb;
38
39 if (!tta || !ttb) {
40 if (tta == ttb)
41 return 0;
42 if (!tta)
43 return -1;
44 return 1;
45 }
46
47 if (i_cal_time_is_date (tta) || i_cal_time_is_date (ttb))
48 return i_cal_time_compare_date_only (tta, ttb);
49
50 return i_cal_time_compare (tta, ttb);
51 }
52
53 static gint
e_timetype_compare_without_date(gconstpointer a,gconstpointer b)54 e_timetype_compare_without_date (gconstpointer a,
55 gconstpointer b)
56 {
57 ICalTime *tta = (ICalTime *) a;
58 ICalTime *ttb = (ICalTime *) b;
59
60 if (!tta || !ttb) {
61 if (tta == ttb)
62 return 0;
63 if (!tta)
64 return -1;
65 return 1;
66 }
67
68 return i_cal_time_compare (tta, ttb);
69 }
70
71 static guint
e_timetype_hash(gconstpointer v)72 e_timetype_hash (gconstpointer v)
73 {
74 const ICalTime *ttv = v;
75 ICalTime *tt;
76 guint value;
77
78 if (!ttv)
79 return 0;
80
81 tt = i_cal_time_convert_to_zone (ttv, i_cal_timezone_get_utc_timezone ());
82
83 value = (i_cal_time_get_year (tt) * 10000) +
84 (i_cal_time_get_month (tt) * 100) +
85 i_cal_time_get_day (tt);
86
87 g_clear_object (&tt);
88
89 return value;
90 }
91
92 typedef struct _EInstanceTime {
93 ICalTime *tt;
94 gboolean duration_set;
95 gint duration_days;
96 gint duration_seconds;
97 } EInstanceTime;
98
99 static EInstanceTime *
e_instance_time_new(const ICalTime * tt,const ICalDuration * pduration)100 e_instance_time_new (const ICalTime *tt,
101 const ICalDuration *pduration)
102 {
103 EInstanceTime *it;
104 ICalDuration *duration = (ICalDuration *) pduration;
105
106 if (!tt)
107 return NULL;
108
109 it = g_slice_new0 (EInstanceTime);
110
111 it->tt = i_cal_time_clone (tt);
112 it->duration_set = duration && !i_cal_duration_is_null_duration (duration);
113
114 if (it->duration_set) {
115 gint64 dur;
116
117 g_warn_if_fail (!i_cal_duration_is_neg (duration));
118
119 dur = (gint64) (i_cal_duration_get_weeks (duration) * 7 + i_cal_duration_get_days (duration)) * (24 * 60 * 60) +
120 (i_cal_duration_get_hours (duration) * 60 * 60) +
121 (i_cal_duration_get_minutes (duration) * 60) +
122 i_cal_duration_get_seconds (duration);
123
124 it->duration_days = dur / (24 * 60 * 60);
125 it->duration_seconds = dur % (24 * 60 * 60);
126 }
127
128 return it;
129 }
130
131 static void
e_instance_time_free(gpointer ptr)132 e_instance_time_free (gpointer ptr)
133 {
134 EInstanceTime *it = ptr;
135
136 if (it) {
137 g_clear_object (&it->tt);
138 g_slice_free (EInstanceTime, it);
139 }
140 }
141
142 static gint
e_instance_time_compare(gconstpointer a,gconstpointer b)143 e_instance_time_compare (gconstpointer a,
144 gconstpointer b)
145 {
146 const EInstanceTime *ait = a;
147 const EInstanceTime *bit = b;
148
149 if (!ait || !bit) {
150 if (ait == bit)
151 return 0;
152
153 if (!ait)
154 return -1;
155
156 return 1;
157 }
158
159 return e_timetype_compare (ait->tt, bit->tt);
160 }
161
162 static guint
e_instance_time_hash(gconstpointer v)163 e_instance_time_hash (gconstpointer v)
164 {
165 const EInstanceTime *it = v;
166
167 if (!it)
168 return 0;
169
170 return e_timetype_hash (it->tt);
171 }
172
173 static gboolean
e_instance_time_equal(gconstpointer v1,gconstpointer v2)174 e_instance_time_equal (gconstpointer v1,
175 gconstpointer v2)
176 {
177 return e_instance_time_compare (v1, v2) == 0;
178 }
179
180 static gint
e_instance_time_compare_ptr_array(gconstpointer a,gconstpointer b)181 e_instance_time_compare_ptr_array (gconstpointer a,
182 gconstpointer b)
183 {
184 gconstpointer *aa = (gconstpointer *) a, *bb = (gconstpointer *) b;
185
186 return e_instance_time_compare (*aa, *bb);
187 }
188
189 static gboolean
ensure_timezone(ICalComponent * comp,ICalTime * tt,ICalPropertyKind prop_kind,ICalProperty * prop,ECalRecurResolveTimezoneCb get_tz_callback,gpointer get_tz_callback_user_data,ICalTimezone * default_timezone,GHashTable ** pcached_zones,GCancellable * cancellable,GError ** error)190 ensure_timezone (ICalComponent *comp,
191 ICalTime *tt,
192 ICalPropertyKind prop_kind,
193 ICalProperty *prop,
194 ECalRecurResolveTimezoneCb get_tz_callback,
195 gpointer get_tz_callback_user_data,
196 ICalTimezone *default_timezone,
197 GHashTable **pcached_zones,
198 GCancellable *cancellable,
199 GError **error)
200 {
201 ICalParameter *param;
202 ICalTimezone *zone;
203 const gchar *tzid;
204 GError *local_error = NULL;
205
206 if (g_cancellable_set_error_if_cancelled (cancellable, error))
207 return FALSE;
208
209 if (!tt)
210 return TRUE;
211
212 /* Do not trust the 'zone' set on the structure, as it can come from
213 a different icalcomponent and cause use-after-free. */
214 zone = i_cal_time_get_timezone (tt);
215 if (zone != i_cal_timezone_get_utc_timezone ())
216 i_cal_time_set_timezone (tt, NULL);
217
218 if (i_cal_time_is_utc (tt))
219 return TRUE;
220
221 i_cal_time_set_timezone (tt, default_timezone);
222
223 if (i_cal_time_is_date (tt))
224 return TRUE;
225
226 if (!prop)
227 prop = i_cal_component_get_first_property (comp, prop_kind);
228 else
229 g_object_ref (prop);
230
231 /* DTEND can be computed from DTSTART and DURATION, thus use TZID from DTSTART,
232 in case DTEND is not present. */
233 if (!prop && prop_kind == I_CAL_DTEND_PROPERTY)
234 prop = i_cal_component_get_first_property (comp, I_CAL_DTSTART_PROPERTY);
235
236 if (!prop)
237 return TRUE;
238
239 param = i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER);
240 if (!param) {
241 g_object_unref (prop);
242 return TRUE;
243 }
244
245 tzid = i_cal_parameter_get_tzid (param);
246 if (!tzid || !*tzid) {
247 g_object_unref (param);
248 g_object_unref (prop);
249 return TRUE;
250 }
251
252 if (get_tz_callback) {
253 zone = NULL;
254
255 if (*pcached_zones)
256 zone = g_hash_table_lookup (*pcached_zones, tzid);
257
258 if (!zone) {
259 zone = get_tz_callback (tzid, get_tz_callback_user_data, cancellable, &local_error);
260 if (zone) {
261 if (!*pcached_zones)
262 *pcached_zones = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
263
264 g_hash_table_insert (*pcached_zones, g_strdup (tzid), g_object_ref (zone));
265 }
266 }
267
268 i_cal_time_set_timezone (tt, zone);
269 } else
270 i_cal_time_set_timezone (tt, NULL);
271
272 zone = i_cal_time_get_timezone (tt);
273 if (!zone)
274 i_cal_time_set_timezone (tt, default_timezone);
275
276 g_object_unref (param);
277 g_object_unref (prop);
278
279 /* Timezone not found is not a fatal error */
280 if (g_error_matches (local_error, E_CAL_CLIENT_ERROR, E_CAL_CLIENT_ERROR_OBJECT_NOT_FOUND))
281 g_clear_error (&local_error);
282
283 if (local_error) {
284 g_propagate_error (error, local_error);
285 return FALSE;
286 }
287
288 return TRUE;
289 }
290
291 static void
apply_duration(ICalTime * tt,ICalDuration * duration)292 apply_duration (ICalTime *tt,
293 ICalDuration *duration)
294 {
295 gint64 days, seconds;
296
297 g_return_if_fail (tt != NULL);
298 g_return_if_fail (duration != NULL);
299
300 if (i_cal_duration_is_null_duration (duration))
301 return;
302
303 days = (gint64) i_cal_duration_get_days (duration) + 7 * ((gint64) i_cal_duration_get_weeks (duration));
304 seconds = ((gint64) i_cal_duration_get_hours (duration) * 60 * 60) +
305 ((gint64) i_cal_duration_get_minutes (duration) * 60) +
306 i_cal_duration_get_seconds (duration);
307 days += seconds / (24 * 60 * 60);
308 seconds = seconds % (24 * 60 * 60);
309
310 if (seconds != 0)
311 i_cal_time_set_is_date (tt, FALSE);
312
313 i_cal_time_adjust (tt,
314 (i_cal_duration_is_neg (duration) ? -1 : 1) * ((gint) days),
315 0, 0,
316 (i_cal_duration_is_neg (duration) ? -1 : 1) * ((gint) seconds));
317 }
318
319 static gboolean
intersects_interval(const ICalTime * tt,const ICalDuration * duration,gint default_duration_days,gint default_duration_seconds,const ICalTime * interval_start,const ICalTime * interval_end)320 intersects_interval (const ICalTime *tt,
321 const ICalDuration *duration,
322 gint default_duration_days,
323 gint default_duration_seconds,
324 const ICalTime *interval_start,
325 const ICalTime *interval_end)
326 {
327 ICalTime *ttstart, *ttend;
328 gboolean res;
329
330 if (!tt || i_cal_time_is_null_time (tt) || !interval_start || !interval_end)
331 return FALSE;
332
333 ttstart = i_cal_time_clone (tt);
334 ttend = i_cal_time_clone (ttstart);
335
336 if (duration && !i_cal_duration_is_null_duration ((ICalDuration *) duration)) {
337 apply_duration (ttend, (ICalDuration *) duration);
338 } else if (default_duration_days || default_duration_seconds) {
339 if (default_duration_seconds != 0) {
340 i_cal_time_set_is_date (ttend, FALSE);
341 }
342
343 i_cal_time_adjust (ttend, default_duration_days, 0, 0, default_duration_seconds);
344 }
345
346 res = e_timetype_compare_without_date (ttstart, interval_end) < 0 &&
347 e_timetype_compare_without_date (interval_start, ttend) < 0;
348
349 g_clear_object (&ttstart);
350 g_clear_object (&ttend);
351
352 return res;
353 }
354
355 /**
356 * e_cal_recur_generate_instances_sync:
357 * @icalcomp: an #ICalComponent
358 * @interval_start: an interval start, for which generate instances
359 * @interval_end: an interval end, for which generate instances
360 * @callback: (scope call): a callback to be called for each instance
361 * @callback_user_data: (closure callback): user data for @callback
362 * @get_tz_callback: (scope call): a callback to call when resolving timezone
363 * @get_tz_callback_user_data: (closure get_tz_callback): user data for @get_tz_callback
364 * @default_timezone: a default #ICalTimezone
365 * @cancellable: a #GCancellable; can be %NULL
366 * @error: a #GError to set an error, if any
367 *
368 * Calls the given callback function for each occurrence of the event that
369 * intersects the range between the given @start and @end times (the end time is
370 * not included). Note that the occurrences may start before the given start
371 * time.
372 *
373 * If the callback routine returns FALSE the occurrence generation stops.
374 *
375 * The start and end times are required valid times, start before end time.
376 *
377 * The @get_tz_callback is used to resolve references to timezones. It is passed
378 * a TZID and should return the ICalTimezone * corresponding to that TZID. We need to
379 * do this as we access timezones in different ways on the client & server.
380 *
381 * The default_timezone argument is used for DTSTART or DTEND properties that
382 * are DATE values or do not have a TZID (i.e. floating times).
383 *
384 * Returns: %TRUE if successful (when all instances had been returned), %FALSE otherwise.
385 *
386 * Since: 3.20
387 **/
388 gboolean
e_cal_recur_generate_instances_sync(ICalComponent * icalcomp,ICalTime * interval_start,ICalTime * interval_end,ECalRecurInstanceCb callback,gpointer callback_user_data,ECalRecurResolveTimezoneCb get_tz_callback,gpointer get_tz_callback_user_data,ICalTimezone * default_timezone,GCancellable * cancellable,GError ** error)389 e_cal_recur_generate_instances_sync (ICalComponent *icalcomp,
390 ICalTime *interval_start,
391 ICalTime *interval_end,
392 ECalRecurInstanceCb callback,
393 gpointer callback_user_data,
394 ECalRecurResolveTimezoneCb get_tz_callback,
395 gpointer get_tz_callback_user_data,
396 ICalTimezone *default_timezone,
397 GCancellable *cancellable,
398 GError **error)
399 {
400 ICalTime *dtstart, *dtend, *next;
401 ICalProperty *prop;
402 gint64 duration_days, duration_seconds;
403 GHashTable *times, *cached_zones = NULL;
404 gboolean success = TRUE;
405
406 g_return_val_if_fail (icalcomp != NULL, FALSE);
407 g_return_val_if_fail (callback != NULL, FALSE);
408 g_return_val_if_fail (interval_start != NULL, FALSE);
409 g_return_val_if_fail (interval_end != NULL, FALSE);
410 g_return_val_if_fail (i_cal_time_compare (interval_start, interval_end) < 0, FALSE);
411
412 if (g_cancellable_set_error_if_cancelled (cancellable, error))
413 return FALSE;
414
415 times = g_hash_table_new_full (e_instance_time_hash, e_instance_time_equal, e_instance_time_free, NULL);
416
417 dtstart = i_cal_component_get_dtstart (icalcomp);
418 success = ensure_timezone (icalcomp, dtstart, I_CAL_DTSTART_PROPERTY, NULL,
419 get_tz_callback, get_tz_callback_user_data, default_timezone, &cached_zones, cancellable, error);
420
421 duration_seconds = 0;
422 dtend = i_cal_component_get_dtend (icalcomp);
423
424 if (!dtend || i_cal_time_is_null_time (dtend)) {
425 g_clear_object (&dtend);
426
427 dtend = i_cal_component_get_due (icalcomp);
428 if (!dtend || i_cal_time_is_null_time (dtend)) {
429 ICalDuration *comp_duration;
430
431 g_clear_object (&dtend);
432
433 comp_duration = i_cal_component_get_duration (icalcomp);
434
435 if (comp_duration && !i_cal_duration_is_null_duration (comp_duration)) {
436 dtend = i_cal_time_clone (dtstart);
437
438 apply_duration (dtend, comp_duration);
439 }
440
441 g_clear_object (&comp_duration);
442 }
443
444 /* This can happen for tasks without start date */
445 if (dtend && i_cal_time_is_null_time (dtstart) && !i_cal_time_is_null_time (dtend)) {
446 g_clear_object (&dtstart);
447 dtstart = i_cal_time_clone (dtend);
448 }
449
450 /* If there is no DTEND, then if DTSTART is a DATE-TIME value
451 * we use the same time (so we have a single point in time).
452 * If DTSTART is a DATE value we add 1 day. */
453 if (!dtend || i_cal_time_is_null_time (dtend)) {
454 g_clear_object (&dtend);
455 dtend = i_cal_time_clone (dtstart);
456
457 if (i_cal_time_is_date (dtend))
458 i_cal_time_adjust (dtend, 1, 0, 0, 0);
459 }
460 } else {
461 /* This can happen for tasks without start date */
462 if (dtend && i_cal_time_is_null_time (dtstart) && !i_cal_time_is_null_time (dtend)) {
463 g_clear_object (&dtstart);
464 dtstart = i_cal_time_clone (dtend);
465 }
466
467 /* If both DTSTART and DTEND are DATE values, and they are the
468 * same day, we add 1 day to DTEND. This means that most
469 * events created with the old Evolution behavior will still
470 * work OK. I'm not sure what Outlook does in this case. */
471 if (i_cal_time_is_date (dtstart) && i_cal_time_is_date (dtend)) {
472 if (i_cal_time_compare_date_only (dtstart, dtend) == 0) {
473 i_cal_time_adjust (dtend, 1, 0, 0, 0);
474 }
475 }
476 }
477
478 if (success && dtend && !i_cal_time_is_null_time (dtend)) {
479 ICalTimezone *dtstart_zone, *dtend_zone;
480
481 success = ensure_timezone (icalcomp, dtend, I_CAL_DTEND_PROPERTY, NULL,
482 get_tz_callback, get_tz_callback_user_data, default_timezone, &cached_zones, cancellable, error);
483
484 dtstart_zone = i_cal_time_get_timezone (dtstart);
485 dtend_zone = i_cal_time_get_timezone (dtend);
486
487 duration_seconds = (gint64) i_cal_time_as_timet_with_zone (dtend, dtend_zone) -
488 (gint64) i_cal_time_as_timet_with_zone (dtstart, dtstart_zone);
489
490 if (duration_seconds < 0)
491 duration_seconds = 0;
492 }
493
494 duration_days = duration_seconds / (24 * 60 * 60);
495 duration_seconds = duration_seconds % (24 * 60 * 60);
496
497 if (success && intersects_interval (dtstart, NULL, duration_days, duration_seconds, interval_start, interval_end)) {
498 g_hash_table_insert (times, e_instance_time_new (dtstart, NULL), NULL);
499 }
500
501 /* If this is a detached instance, then use only the DTSTART value */
502 if (success && !e_cal_util_component_has_property (icalcomp, I_CAL_RECURRENCEID_PROPERTY)) {
503 ICalTimezone *dtstart_zone = i_cal_time_get_timezone (dtstart);
504
505 for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_RRULE_PROPERTY);
506 prop && success;
507 g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_RRULE_PROPERTY)) {
508 ICalRecurrence *rrule = i_cal_property_get_rrule (prop);
509 ICalRecurIterator *riter;
510 ICalTime *rrule_until;
511
512 rrule_until = i_cal_recurrence_get_until (rrule);
513
514 if (rrule_until && !i_cal_time_is_null_time (rrule_until)) {
515 success = ensure_timezone (icalcomp, rrule_until, 0, prop,
516 get_tz_callback, get_tz_callback_user_data, dtstart_zone,
517 &cached_zones, cancellable, error);
518 if (!success) {
519 g_clear_object (&rrule_until);
520 g_clear_object (&rrule);
521 break;
522 }
523 }
524
525 if (rrule_until && !i_cal_time_is_null_time (rrule_until) &&
526 i_cal_time_is_date (rrule_until) && !i_cal_time_is_date (dtstart)) {
527 i_cal_time_adjust (rrule_until, 1, 0, 0, 0);
528 i_cal_time_set_is_date (rrule_until, FALSE);
529 i_cal_time_set_time (rrule_until, 0, 0, 0);
530
531 if (!i_cal_time_get_timezone (rrule_until) && !i_cal_time_is_utc (rrule_until))
532 i_cal_time_set_timezone (rrule_until, dtstart_zone);
533 }
534
535 if (rrule_until && !i_cal_time_is_null_time (rrule_until))
536 i_cal_recurrence_set_until (rrule, rrule_until);
537 g_clear_object (&rrule_until);
538
539 riter = i_cal_recur_iterator_new (rrule, dtstart);
540 if (riter) {
541 ICalTime *prev = NULL;
542
543 for (next = i_cal_recur_iterator_next (riter);
544 next && !i_cal_time_is_null_time (next) && i_cal_time_compare (next, interval_end) <= 0;
545 g_object_unref (next), next = i_cal_recur_iterator_next (riter)) {
546 if (prev && !i_cal_time_is_null_time (prev) &&
547 i_cal_time_compare (next, prev) == 0)
548 break;
549
550 g_clear_object (&prev);
551 prev = i_cal_time_clone (next);
552
553 if (intersects_interval (next, NULL, duration_days, duration_seconds, interval_start, interval_end)) {
554 g_hash_table_insert (times, e_instance_time_new (next, NULL), NULL);
555 }
556 }
557
558 g_clear_object (&riter);
559 g_clear_object (&prev);
560 g_clear_object (&next);
561 }
562
563 g_clear_object (&rrule);
564 }
565
566 g_clear_object (&prop);
567
568 for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_RDATE_PROPERTY);
569 prop && success;
570 g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_RDATE_PROPERTY)) {
571 ICalDatetimeperiod *rdate = i_cal_property_get_rdate (prop);
572 ICalTime *tt = NULL;
573 ICalDuration *duration = i_cal_duration_new_null_duration ();
574
575 tt = i_cal_datetimeperiod_get_time (rdate);
576 if (!tt || i_cal_time_is_null_time (tt)) {
577 ICalPeriod *period;
578
579 g_clear_object (&tt);
580 period = i_cal_datetimeperiod_get_period (rdate);
581
582 if (period && !i_cal_period_is_null_period (period)) {
583 ICalTime *pend;
584
585 tt = i_cal_period_get_start (period);
586 pend = i_cal_period_get_end (period);
587
588 if (pend && !i_cal_time_is_null_time (pend)) {
589 time_t diff;
590
591 diff = i_cal_time_as_timet (pend) - i_cal_time_as_timet (tt);
592 if (diff < 0) {
593 i_cal_duration_set_is_neg (duration, TRUE);
594 diff = diff * (-1);
595 }
596
597 #define set_and_dec(member, num) G_STMT_START { \
598 i_cal_duration_set_ ## member (duration, diff / (num)); \
599 diff = diff % (num); \
600 } G_STMT_END
601 set_and_dec (weeks, 7 * 24 * 60 * 60);
602 set_and_dec (days, 24 * 60 * 60);
603 set_and_dec (hours, 60 * 60);
604 set_and_dec (minutes, 60);
605 set_and_dec (seconds, 1);
606 #undef set_and_dec
607
608 g_warn_if_fail (diff == 0);
609 } else {
610 g_clear_object (&duration);
611 duration = i_cal_period_get_duration (period);
612 }
613
614 if (duration && !i_cal_duration_is_null_duration (duration) &&
615 tt && !i_cal_time_is_null_time (tt)) {
616 if (i_cal_duration_is_neg (duration)) {
617 apply_duration (tt, duration);
618
619 i_cal_duration_set_is_neg (duration, FALSE);
620 }
621 }
622
623 g_clear_object (&pend);
624 }
625
626 g_clear_object (&period);
627 }
628
629 if (tt && !i_cal_time_is_null_time (tt)) {
630 success = ensure_timezone (icalcomp, tt, 0, prop,
631 get_tz_callback, get_tz_callback_user_data, dtstart_zone,
632 &cached_zones, cancellable, error);
633 if (!success) {
634 g_clear_object (&duration);
635 g_clear_object (&rdate);
636 g_clear_object (&tt);
637 break;
638 }
639
640 if (intersects_interval (tt, duration, duration_days, duration_seconds, interval_start, interval_end)) {
641 g_hash_table_insert (times, e_instance_time_new (tt, duration), NULL);
642 }
643 }
644
645 g_clear_object (&duration);
646 g_clear_object (&rdate);
647 g_clear_object (&tt);
648 }
649
650 g_clear_object (&prop);
651
652 for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_EXRULE_PROPERTY);
653 prop && success;
654 g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_EXRULE_PROPERTY)) {
655 ICalRecurrence *exrule = i_cal_property_get_exrule (prop);
656 ICalRecurIterator *riter;
657 ICalTime *exrule_until;
658
659 exrule_until = i_cal_recurrence_get_until (exrule);
660
661 if (exrule_until && !i_cal_time_is_null_time (exrule_until)) {
662 success = ensure_timezone (icalcomp, exrule_until, 0, prop,
663 get_tz_callback, get_tz_callback_user_data, dtstart_zone,
664 &cached_zones, cancellable, error);
665 if (!success) {
666 g_clear_object (&exrule_until);
667 g_clear_object (&exrule);
668 break;
669 }
670 }
671
672 if (exrule_until && !i_cal_time_is_null_time (exrule_until) &&
673 i_cal_time_is_date (exrule_until) && !i_cal_time_is_date (dtstart)) {
674 i_cal_time_adjust (exrule_until, 1, 0, 0, 0);
675 i_cal_time_set_is_date (exrule_until, FALSE);
676 i_cal_time_set_time (exrule_until, 0, 0, 0);
677
678 if (!i_cal_time_get_timezone (exrule_until) && !i_cal_time_is_utc (exrule_until))
679 i_cal_time_set_timezone (exrule_until, dtstart_zone);
680 }
681
682 if (exrule_until && !i_cal_time_is_null_time (exrule_until))
683 i_cal_recurrence_set_until (exrule, exrule_until);
684 g_clear_object (&exrule_until);
685
686 riter = i_cal_recur_iterator_new (exrule, dtstart);
687 if (riter) {
688 ICalTime *prev = NULL;
689
690 for (next = i_cal_recur_iterator_next (riter);
691 next && !i_cal_time_is_null_time (next) && i_cal_time_compare (next, interval_end) <= 0;
692 g_object_unref (next), next = i_cal_recur_iterator_next (riter)) {
693 if (prev && !i_cal_time_is_null_time (prev) &&
694 i_cal_time_compare (next, prev) == 0)
695 break;
696 g_clear_object (&prev);
697 prev = i_cal_time_clone (next);
698
699 if (intersects_interval (next, NULL, duration_days, duration_seconds, interval_start, interval_end)) {
700 EInstanceTime it;
701
702 it.tt = next;
703 it.duration_set = FALSE;
704
705 g_hash_table_remove (times, &it);
706 }
707 }
708
709 g_clear_object (&riter);
710 g_clear_object (&prev);
711 g_clear_object (&next);
712 }
713
714 g_clear_object (&exrule);
715 }
716
717 g_clear_object (&prop);
718
719 for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_EXDATE_PROPERTY);
720 prop && success;
721 g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_EXDATE_PROPERTY)) {
722 ICalTime *exdate = i_cal_property_get_exdate (prop);
723
724 if (!exdate || i_cal_time_is_null_time (exdate)) {
725 g_clear_object (&exdate);
726 continue;
727 }
728
729 success = ensure_timezone (icalcomp, exdate, 0, prop,
730 get_tz_callback, get_tz_callback_user_data, dtstart_zone,
731 &cached_zones, cancellable, error);
732 if (!success) {
733 g_clear_object (&exdate);
734 break;
735 }
736
737 if (!i_cal_time_get_timezone (exdate) && !i_cal_time_is_utc (exdate))
738 i_cal_time_set_timezone (exdate, dtstart_zone);
739
740 if (intersects_interval (exdate, NULL, duration_days, duration_seconds, interval_start, interval_end)) {
741 EInstanceTime it;
742
743 it.tt = exdate;
744 it.duration_set = FALSE;
745
746 g_hash_table_remove (times, &it);
747 }
748
749 g_clear_object (&exdate);
750 }
751
752 g_clear_object (&prop);
753 }
754
755 if (success && g_hash_table_size (times) > 0) {
756 GPtrArray *times_array;
757 GHashTableIter hiter;
758 gpointer key, value;
759 gint ii;
760
761 times_array = g_ptr_array_sized_new (g_hash_table_size (times));
762
763 g_hash_table_iter_init (&hiter, times);
764 while (g_hash_table_iter_next (&hiter, &key, &value)) {
765 EInstanceTime *it = key;
766
767 if (!it)
768 continue;
769
770 g_ptr_array_add (times_array, it);
771 }
772
773 g_ptr_array_sort (times_array, e_instance_time_compare_ptr_array);
774
775 for (ii = 0; ii < times_array->len; ii++) {
776 EInstanceTime *it = g_ptr_array_index (times_array, ii);
777 ICalTime *endtt;
778 gint dur_days = duration_days, dur_seconds = duration_seconds;
779
780 if (!it)
781 continue;
782
783 endtt = i_cal_time_clone (it->tt);
784
785 if (it->duration_set) {
786 dur_days = it->duration_days;
787 dur_seconds = it->duration_seconds;
788 }
789
790 if (dur_seconds != 0)
791 i_cal_time_set_is_date (endtt, FALSE);
792
793 i_cal_time_adjust (endtt, dur_days, 0, 0, dur_seconds);
794
795 if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
796 success = FALSE;
797 g_clear_object (&endtt);
798 break;
799 }
800
801 success = callback (icalcomp, it->tt, endtt, callback_user_data, cancellable, error);
802
803 g_clear_object (&endtt);
804
805 if (!success)
806 break;
807 }
808
809 g_ptr_array_free (times_array, TRUE);
810 }
811
812 g_hash_table_destroy (times);
813
814 if (cached_zones)
815 g_hash_table_destroy (cached_zones);
816
817 g_clear_object (&dtstart);
818 g_clear_object (&dtend);
819
820 return success;
821 }
822
823 /*
824 * Introduction to The Recurrence Generation Functions:
825 *
826 * Note: This is pretty complicated. See the iCalendar spec (RFC 2445) for
827 * the specification of the recurrence rules and lots of examples
828 * (sections 4.3.10 & 4.8.5). We also want to support the older
829 * vCalendar spec, though this should be easy since it is basically a
830 * subset of iCalendar.
831 *
832 * o An iCalendar event can have any number of recurrence rules specifying
833 * occurrences of the event, as well as dates & times of specific
834 * occurrences. It can also have any number of recurrence rules and
835 * specific dates & times specifying exceptions to the occurrences.
836 * So we first merge all the occurrences generated, eliminating any
837 * duplicates, then we generate all the exceptions and remove these to
838 * form the final set of occurrences.
839 *
840 * o There are 7 frequencies of occurrences: YEARLY, MONTHLY, WEEKLY, DAILY,
841 * HOURLY, MINUTELY & SECONDLY. The 'interval' property specifies the
842 * multiples of the frequency which we step by. We generate a 'set' of
843 * occurrences for each period defined by the frequency & interval.
844 * So for a YEARLY frequency with an interval of 3, we generate a set of
845 * occurrences for every 3rd year. We use complete years here - any
846 * generated occurrences that occur before the event's start (or after its
847 * end) are just discarded.
848 *
849 * o There are 8 frequency modifiers: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY,
850 * BYDAY, BYHOUR, BYMINUTE & BYSECOND. These can either add extra occurrences
851 * or filter out occurrences. For example 'FREQ=YEARLY;BYMONTH=1,2' produces
852 * 2 occurrences for each year rather than the default 1. And
853 * 'FREQ=DAILY;BYMONTH=1' filters out all occurrences except those in Jan.
854 * If the modifier works on periods which are less than the recurrence
855 * frequency, then extra occurrences are added, otherwise occurrences are
856 * filtered. So we have 2 functions for each modifier - one to expand events
857 * and the other to filter. We use a table of functions for each frequency
858 * which points to the appropriate function to use for each modifier.
859 *
860 * o Any number of frequency modifiers can be used in a recurrence rule.
861 * (Though the iCalendar spec says that BYWEEKNO can only be used in a YEARLY
862 * rule, and some modifiers aren't appropriate for some frequencies - e.g.
863 * BYMONTHDAY is not really useful in a WEEKLY frequency, and BYYEARDAY is
864 * not useful in a MONTHLY or WEEKLY frequency).
865 * The frequency modifiers are applied in the order given above. The first 5
866 * modifier rules (BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY & BYDAY) all
867 * produce the days on which the occurrences take place, and so we have to
868 * compute some of these in parallel rather than sequentially, or we may end
869 * up with too many days.
870 *
871 * o Note that some expansion functions may produce days which are invalid,
872 * e.g. 31st September, 30th Feb. These invalid days are removed before the
873 * BYHOUR, BYMINUTE & BYSECOND modifier functions are applied.
874 *
875 * o After the set of occurrences for the frequency interval are generated,
876 * the BYSETPOS property is used to select which of the occurrences are
877 * finally output. If BYSETPOS is not specified then all the occurrences are
878 * output.
879 *
880 *
881 * FIXME: I think there are a few errors in this code:
882 *
883 * 1) I'm not sure it should be generating events in parallel like it says
884 * above. That needs to be checked.
885 *
886 * 2) I didn't think about timezone changes when implementing this. I just
887 * assumed all the occurrences of the event would be in local time.
888 * But when clocks go back or forwards due to daylight-saving time, some
889 * special handling may be needed, especially for the shorter frequencies.
890 * e.g. for a MINUTELY frequency it should probably iterate over all the
891 * minutes before and after clocks go back (i.e. some may be the same local
892 * time but have different UTC offsets). For longer frequencies, if an
893 * occurrence lands on the overlapping or non-existant time when clocks
894 * go back/forward, then it may need to choose which of the times to use
895 * or move the time forward or something. I'm not sure this is clear in the
896 * spec.
897 */
898
899 /* This is the maximum year we will go up to (inclusive). Since we use time_t
900 * values we can't go past 2037 anyway, and some of our VTIMEZONEs may stop
901 * at 2037 as well. */
902 #define MAX_YEAR 2037
903
904 /* Define this for some debugging output. */
905 #if 0
906 #define CAL_OBJ_DEBUG 1
907 #endif
908
909 /* We will use ICalRecurrence instead of this eventually. */
910 typedef struct {
911 ICalRecurrenceFrequency freq;
912
913 gint interval;
914
915 /* Specifies the end of the recurrence, inclusive. No occurrences are
916 * generated after this date. If it is 0, the event recurs forever. */
917 time_t enddate;
918
919 /* WKST property - the week start day: 0 = Monday to 6 = Sunday. */
920 gint week_start_day;
921
922 /* NOTE: I've used GList's here, but it doesn't matter if we use
923 * other data structures like arrays. The code should be easy to
924 * change. So long as it is easy to see if the modifier is set. */
925
926 /* For BYMONTH modifier. A list of GINT_TO_POINTERs, 0-11. */
927 GList *bymonth;
928
929 /* For BYWEEKNO modifier. A list of GINT_TO_POINTERs, [+-]1-53. */
930 GList *byweekno;
931
932 /* For BYYEARDAY modifier. A list of GINT_TO_POINTERs, [+-]1-366. */
933 GList *byyearday;
934
935 /* For BYMONTHDAY modifier. A list of GINT_TO_POINTERs, [+-]1-31. */
936 GList *bymonthday;
937
938 /* For BYDAY modifier. A list of GINT_TO_POINTERs, in pairs.
939 * The first of each pair is the weekday, 0 = Monday to 6 = Sunday.
940 * The second of each pair is the week number[+-]0-53. */
941 GList *byday;
942
943 /* For BYHOUR modifier. A list of GINT_TO_POINTERs, 0-23. */
944 GList *byhour;
945
946 /* For BYMINUTE modifier. A list of GINT_TO_POINTERs, 0-59. */
947 GList *byminute;
948
949 /* For BYSECOND modifier. A list of GINT_TO_POINTERs, 0-60. */
950 GList *bysecond;
951
952 /* For BYSETPOS modifier. A list of GINT_TO_POINTERs, +ve or -ve. */
953 GList *bysetpos;
954 } ECalRecurrence;
955
956 /* This is what we use to pass to all the filter functions. */
957 typedef struct _RecurData RecurData;
958 struct _RecurData {
959 ECalRecurrence *recur;
960
961 /* This is used for the WEEKLY frequency. It is the offset from the
962 * week_start_day. */
963 gint weekday_offset;
964
965 /* This is used for fast lookup in BYMONTH filtering. */
966 guint8 months[12];
967
968 /* This is used for fast lookup in BYYEARDAY filtering. */
969 guint8 yeardays[367], neg_yeardays[367]; /* Days are 1 - 366. */
970
971 /* This is used for fast lookup in BYMONTHDAY filtering. */
972 guint8 monthdays[32], neg_monthdays[32]; /* Days are 1 to 31. */
973
974 /* This is used for fast lookup in BYDAY filtering. */
975 guint8 weekdays[7];
976
977 /* This is used for fast lookup in BYHOUR filtering. */
978 guint8 hours[24];
979
980 /* This is used for fast lookup in BYMINUTE filtering. */
981 guint8 minutes[60];
982
983 /* This is used for fast lookup in BYSECOND filtering. */
984 guint8 seconds[62];
985 };
986
987 /* This is what we use to represent a date & time. */
988 typedef struct _CalObjTime CalObjTime;
989 struct _CalObjTime {
990 guint16 year;
991 guint8 month; /* 0 - 11 */
992 guint8 day; /* 1 - 31 */
993 guint8 hour; /* 0 - 23 */
994 guint8 minute; /* 0 - 59 */
995 guint8 second; /* 0 - 59 (maybe up to 61 for leap seconds) */
996 guint8 flags; /* The meaning of this depends on where the
997 * CalObjTime is used. In most cases this is
998 * set to TRUE to indicate that this is an
999 * RDATE with an end or a duration set.
1000 * In the exceptions code, this is set to TRUE
1001 * to indicate that this is an EXDATE with a
1002 * DATE value. */
1003 };
1004
1005 /* This is what we use to represent specific recurrence dates.
1006 * Note that we assume it starts with a CalObjTime when sorting. */
1007 typedef struct _CalObjRecurrenceDate CalObjRecurrenceDate;
1008 struct _CalObjRecurrenceDate {
1009 CalObjTime start;
1010 ECalComponentPeriod *period;
1011 };
1012
1013 typedef gboolean (*CalObjFindStartFn) (CalObjTime *event_start,
1014 CalObjTime *event_end,
1015 RecurData *recur_data,
1016 CalObjTime *interval_start,
1017 CalObjTime *interval_end,
1018 CalObjTime *cotime);
1019 typedef gboolean (*CalObjFindNextFn) (CalObjTime *cotime,
1020 CalObjTime *event_end,
1021 RecurData *recur_data,
1022 CalObjTime *interval_end);
1023 typedef GArray * (*CalObjFilterFn) (RecurData *recur_data,
1024 GArray *occs);
1025
1026 typedef struct _ECalRecurVTable ECalRecurVTable;
1027 struct _ECalRecurVTable {
1028 CalObjFindStartFn find_start_position;
1029 CalObjFindNextFn find_next_position;
1030
1031 CalObjFilterFn bymonth_filter;
1032 CalObjFilterFn byweekno_filter;
1033 CalObjFilterFn byyearday_filter;
1034 CalObjFilterFn bymonthday_filter;
1035 CalObjFilterFn byday_filter;
1036 CalObjFilterFn byhour_filter;
1037 CalObjFilterFn byminute_filter;
1038 CalObjFilterFn bysecond_filter;
1039 };
1040
1041 /* This is used to specify which parts of the CalObjTime to compare in
1042 * cal_obj_time_compare (). */
1043 typedef enum {
1044 CALOBJ_YEAR,
1045 CALOBJ_MONTH,
1046 CALOBJ_DAY,
1047 CALOBJ_HOUR,
1048 CALOBJ_MINUTE,
1049 CALOBJ_SECOND
1050 } CalObjTimeComparison;
1051
1052 static void e_cal_recur_generate_instances_of_rule (ECalComponent *comp,
1053 ICalProperty *prop,
1054 time_t start,
1055 time_t end,
1056 ECalRecurInstanceCb cb,
1057 gpointer cb_data,
1058 ECalRecurResolveTimezoneCb tz_cb,
1059 gpointer tz_cb_data,
1060 ICalTimezone *default_timezone);
1061
1062 static ECalRecurrence * e_cal_recur_from_icalproperty (ICalProperty *prop,
1063 gboolean exception,
1064 ICalTimezone *zone,
1065 gboolean convert_end_date);
1066 static gint e_cal_recur_ical_weekday_to_weekday (ICalRecurrenceWeekday day);
1067 static void e_cal_recur_free (ECalRecurrence *r);
1068
1069 static gboolean cal_object_get_rdate_end (CalObjTime *occ,
1070 GArray *rdate_periods,
1071 ICalTimezone *zone);
1072 static void cal_object_compute_duration (CalObjTime *start,
1073 CalObjTime *end,
1074 gint *days,
1075 gint *seconds);
1076
1077 static gboolean generate_instances_for_chunk (ECalComponent *comp,
1078 time_t comp_dtstart,
1079 ICalTimezone *zone,
1080 GSList *rrules,
1081 GSList *rdates,
1082 GSList *exrules,
1083 GSList *exdates,
1084 gboolean single_rule,
1085 CalObjTime *event_start,
1086 time_t interval_start,
1087 CalObjTime *chunk_start,
1088 CalObjTime *chunk_end,
1089 gint duration_days,
1090 gint duration_seconds,
1091 gboolean convert_end_date,
1092 ECalRecurInstanceCb cb,
1093 gpointer cb_data);
1094
1095 static GArray * cal_obj_expand_recurrence (CalObjTime *event_start,
1096 ICalTimezone *zone,
1097 ECalRecurrence *recur,
1098 CalObjTime *interval_start,
1099 CalObjTime *interval_end,
1100 gboolean *finished);
1101
1102 static GArray * cal_obj_generate_set_yearly (RecurData *recur_data,
1103 ECalRecurVTable *vtable,
1104 CalObjTime *occ);
1105 static GArray * cal_obj_generate_set_monthly (RecurData *recur_data,
1106 ECalRecurVTable *vtable,
1107 CalObjTime *occ);
1108 static GArray * cal_obj_generate_set_default (RecurData *recur_data,
1109 ECalRecurVTable *vtable,
1110 CalObjTime *occ);
1111
1112 static gboolean cal_obj_get_vtable (ECalRecurrence *recur,
1113 ECalRecurVTable *out_vtable);
1114 static void cal_obj_initialize_recur_data (RecurData *recur_data,
1115 ECalRecurrence *recur,
1116 CalObjTime *event_start);
1117 static void cal_obj_sort_occurrences (GArray *occs);
1118 static gint cal_obj_time_compare_func (const void *arg1,
1119 const void *arg2);
1120 static void cal_obj_remove_duplicates_and_invalid_dates (GArray *occs);
1121 static void cal_obj_remove_exceptions (GArray *occs,
1122 GArray *ex_occs);
1123 static GArray * cal_obj_bysetpos_filter (ECalRecurrence *recur,
1124 GArray *occs);
1125
1126 static gboolean cal_obj_yearly_find_start_position (CalObjTime *event_start,
1127 CalObjTime *event_end,
1128 RecurData *recur_data,
1129 CalObjTime *interval_start,
1130 CalObjTime *interval_end,
1131 CalObjTime *cotime);
1132 static gboolean cal_obj_yearly_find_next_position (CalObjTime *cotime,
1133 CalObjTime *event_end,
1134 RecurData *recur_data,
1135 CalObjTime *interval_end);
1136
1137 static gboolean cal_obj_monthly_find_start_position (CalObjTime *event_start,
1138 CalObjTime *event_end,
1139 RecurData *recur_data,
1140 CalObjTime *interval_start,
1141 CalObjTime *interval_end,
1142 CalObjTime *cotime);
1143 static gboolean cal_obj_monthly_find_next_position (CalObjTime *cotime,
1144 CalObjTime *event_end,
1145 RecurData *recur_data,
1146 CalObjTime *interval_end);
1147
1148 static gboolean cal_obj_weekly_find_start_position (CalObjTime *event_start,
1149 CalObjTime *event_end,
1150 RecurData *recur_data,
1151 CalObjTime *interval_start,
1152 CalObjTime *interval_end,
1153 CalObjTime *cotime);
1154 static gboolean cal_obj_weekly_find_next_position (CalObjTime *cotime,
1155 CalObjTime *event_end,
1156 RecurData *recur_data,
1157 CalObjTime *interval_end);
1158
1159 static gboolean cal_obj_daily_find_start_position (CalObjTime *event_start,
1160 CalObjTime *event_end,
1161 RecurData *recur_data,
1162 CalObjTime *interval_start,
1163 CalObjTime *interval_end,
1164 CalObjTime *cotime);
1165 static gboolean cal_obj_daily_find_next_position (CalObjTime *cotime,
1166 CalObjTime *event_end,
1167 RecurData *recur_data,
1168 CalObjTime *interval_end);
1169
1170 static gboolean cal_obj_hourly_find_start_position (CalObjTime *event_start,
1171 CalObjTime *event_end,
1172 RecurData *recur_data,
1173 CalObjTime *interval_start,
1174 CalObjTime *interval_end,
1175 CalObjTime *cotime);
1176 static gboolean cal_obj_hourly_find_next_position (CalObjTime *cotime,
1177 CalObjTime *event_end,
1178 RecurData *recur_data,
1179 CalObjTime *interval_end);
1180
1181 static gboolean cal_obj_minutely_find_start_position (CalObjTime *event_start,
1182 CalObjTime *event_end,
1183 RecurData *recur_data,
1184 CalObjTime *interval_start,
1185 CalObjTime *interval_end,
1186 CalObjTime *cotime);
1187 static gboolean cal_obj_minutely_find_next_position (CalObjTime *cotime,
1188 CalObjTime *event_end,
1189 RecurData *recur_data,
1190 CalObjTime *interval_end);
1191
1192 static gboolean cal_obj_secondly_find_start_position (CalObjTime *event_start,
1193 CalObjTime *event_end,
1194 RecurData *recur_data,
1195 CalObjTime *interval_start,
1196 CalObjTime *interval_end,
1197 CalObjTime *cotime);
1198 static gboolean cal_obj_secondly_find_next_position (CalObjTime *cotime,
1199 CalObjTime *event_end,
1200 RecurData *recur_data,
1201 CalObjTime *interval_end);
1202
1203 static GArray * cal_obj_bymonth_expand (RecurData *recur_data,
1204 GArray *occs);
1205 static GArray * cal_obj_bymonth_filter (RecurData *recur_data,
1206 GArray *occs);
1207 static GArray * cal_obj_byweekno_expand (RecurData *recur_data,
1208 GArray *occs);
1209 #if 0
1210 /* This isn't used at present. */
1211 static GArray * cal_obj_byweekno_filter (RecurData *recur_data,
1212 GArray *occs);
1213 #endif
1214 static GArray * cal_obj_byyearday_expand (RecurData *recur_data,
1215 GArray *occs);
1216 static GArray * cal_obj_byyearday_filter (RecurData *recur_data,
1217 GArray *occs);
1218 static GArray * cal_obj_bymonthday_expand (RecurData *recur_data,
1219 GArray *occs);
1220 static GArray * cal_obj_bymonthday_filter (RecurData *recur_data,
1221 GArray *occs);
1222 static GArray * cal_obj_byday_expand_yearly (RecurData *recur_data,
1223 GArray *occs);
1224 static GArray * cal_obj_byday_expand_monthly (RecurData *recur_data,
1225 GArray *occs);
1226 static GArray * cal_obj_byday_expand_weekly (RecurData *recur_data,
1227 GArray *occs);
1228 static GArray * cal_obj_byday_filter (RecurData *recur_data,
1229 GArray *occs);
1230 static GArray * cal_obj_byhour_expand (RecurData *recur_data,
1231 GArray *occs);
1232 static GArray * cal_obj_byhour_filter (RecurData *recur_data,
1233 GArray *occs);
1234 static GArray * cal_obj_byminute_expand (RecurData *recur_data,
1235 GArray *occs);
1236 static GArray * cal_obj_byminute_filter (RecurData *recur_data,
1237 GArray *occs);
1238 static GArray * cal_obj_bysecond_expand (RecurData *recur_data,
1239 GArray *occs);
1240 static GArray * cal_obj_bysecond_filter (RecurData *recur_data,
1241 GArray *occs);
1242
1243 static void cal_obj_time_add_months (CalObjTime *cotime,
1244 gint months);
1245 static void cal_obj_time_add_days (CalObjTime *cotime,
1246 gint days);
1247 static void cal_obj_time_add_hours (CalObjTime *cotime,
1248 gint hours);
1249 static void cal_obj_time_add_minutes (CalObjTime *cotime,
1250 gint minutes);
1251 static void cal_obj_time_add_seconds (CalObjTime *cotime,
1252 gint seconds);
1253 static gint cal_obj_time_compare (CalObjTime *cotime1,
1254 CalObjTime *cotime2,
1255 CalObjTimeComparison type);
1256 static gint cal_obj_time_weekday (CalObjTime *cotime);
1257 static gint cal_obj_time_weekday_offset (CalObjTime *cotime,
1258 ECalRecurrence *recur);
1259 static gint cal_obj_time_day_of_year (CalObjTime *cotime);
1260 static void cal_obj_time_find_first_week (CalObjTime *cotime,
1261 RecurData *recur_data);
1262 static void cal_object_time_from_time (CalObjTime *cotime,
1263 time_t t,
1264 ICalTimezone *zone);
1265 static gint cal_obj_date_only_compare_func (gconstpointer arg1,
1266 gconstpointer arg2);
1267 static gboolean e_cal_recur_ensure_rule_end_date (ECalComponent *comp,
1268 ICalProperty *prop,
1269 gboolean exception,
1270 gboolean refresh,
1271 ECalRecurResolveTimezoneCb tz_cb,
1272 gpointer tz_cb_data,
1273 GCancellable *cancellable,
1274 GError **error);
1275 static gboolean e_cal_recur_ensure_rule_end_date_cb (ICalComponent *comp,
1276 ICalTime *instance_start,
1277 ICalTime *instance_end,
1278 gpointer user_data,
1279 GCancellable *cancellable,
1280 GError **error);
1281 static time_t e_cal_recur_get_rule_end_date (ICalProperty *prop,
1282 ICalTimezone *default_timezone);
1283 static void e_cal_recur_set_rule_end_date (ICalProperty *prop,
1284 time_t end_date);
1285
1286 #ifdef CAL_OBJ_DEBUG
1287 static const gchar * cal_obj_time_to_string (CalObjTime *cotime);
1288 #endif
1289
1290 static ECalRecurVTable cal_obj_yearly_vtable = {
1291 cal_obj_yearly_find_start_position,
1292 cal_obj_yearly_find_next_position,
1293
1294 cal_obj_bymonth_expand,
1295 cal_obj_byweekno_expand,
1296 cal_obj_byyearday_expand,
1297 cal_obj_bymonthday_expand,
1298 cal_obj_byday_expand_yearly,
1299 cal_obj_byhour_expand,
1300 cal_obj_byminute_expand,
1301 cal_obj_bysecond_expand
1302 };
1303
1304 static ECalRecurVTable cal_obj_monthly_vtable = {
1305 cal_obj_monthly_find_start_position,
1306 cal_obj_monthly_find_next_position,
1307
1308 cal_obj_bymonth_filter,
1309 NULL, /* BYWEEKNO is only applicable to YEARLY frequency. */
1310 NULL, /* BYYEARDAY is not useful in a MONTHLY frequency. */
1311 cal_obj_bymonthday_filter,
1312 cal_obj_byday_expand_monthly,
1313 cal_obj_byhour_expand,
1314 cal_obj_byminute_expand,
1315 cal_obj_bysecond_expand
1316 };
1317
1318 static ECalRecurVTable cal_obj_weekly_vtable = {
1319 cal_obj_weekly_find_start_position,
1320 cal_obj_weekly_find_next_position,
1321
1322 cal_obj_bymonth_filter,
1323 NULL, /* BYWEEKNO is only applicable to YEARLY frequency. */
1324 NULL, /* BYYEARDAY is not useful in a WEEKLY frequency. */
1325 NULL, /* BYMONTHDAY is not useful in a WEEKLY frequency. */
1326 cal_obj_byday_expand_weekly,
1327 cal_obj_byhour_expand,
1328 cal_obj_byminute_expand,
1329 cal_obj_bysecond_expand
1330 };
1331
1332 static ECalRecurVTable cal_obj_daily_vtable = {
1333 cal_obj_daily_find_start_position,
1334 cal_obj_daily_find_next_position,
1335
1336 cal_obj_bymonth_filter,
1337 NULL, /* BYWEEKNO is only applicable to YEARLY frequency. */
1338 cal_obj_byyearday_filter,
1339 cal_obj_bymonthday_filter,
1340 cal_obj_byday_filter,
1341 cal_obj_byhour_expand,
1342 cal_obj_byminute_expand,
1343 cal_obj_bysecond_expand
1344 };
1345
1346 static ECalRecurVTable cal_obj_hourly_vtable = {
1347 cal_obj_hourly_find_start_position,
1348 cal_obj_hourly_find_next_position,
1349
1350 cal_obj_bymonth_filter,
1351 NULL, /* BYWEEKNO is only applicable to YEARLY frequency. */
1352 cal_obj_byyearday_filter,
1353 cal_obj_bymonthday_filter,
1354 cal_obj_byday_filter,
1355 cal_obj_byhour_filter,
1356 cal_obj_byminute_expand,
1357 cal_obj_bysecond_expand
1358 };
1359
1360 static ECalRecurVTable cal_obj_minutely_vtable = {
1361 cal_obj_minutely_find_start_position,
1362 cal_obj_minutely_find_next_position,
1363
1364 cal_obj_bymonth_filter,
1365 NULL, /* BYWEEKNO is only applicable to YEARLY frequency. */
1366 cal_obj_byyearday_filter,
1367 cal_obj_bymonthday_filter,
1368 cal_obj_byday_filter,
1369 cal_obj_byhour_filter,
1370 cal_obj_byminute_filter,
1371 cal_obj_bysecond_expand
1372 };
1373
1374 static ECalRecurVTable cal_obj_secondly_vtable = {
1375 cal_obj_secondly_find_start_position,
1376 cal_obj_secondly_find_next_position,
1377
1378 cal_obj_bymonth_filter,
1379 NULL, /* BYWEEKNO is only applicable to YEARLY frequency. */
1380 cal_obj_byyearday_filter,
1381 cal_obj_bymonthday_filter,
1382 cal_obj_byday_filter,
1383 cal_obj_byhour_filter,
1384 cal_obj_byminute_filter,
1385 cal_obj_bysecond_filter
1386 };
1387
1388 static gboolean
call_instance_callback(ECalRecurInstanceCb cb,ECalComponent * comp,time_t dtstart_time,time_t dtend_time,gpointer cb_data)1389 call_instance_callback (ECalRecurInstanceCb cb,
1390 ECalComponent *comp,
1391 time_t dtstart_time,
1392 time_t dtend_time,
1393 gpointer cb_data)
1394 {
1395 ICalTime *start, *end;
1396 ICalTimezone *utc_zone;
1397 gboolean res;
1398
1399 g_return_val_if_fail (cb != NULL, FALSE);
1400
1401 utc_zone = i_cal_timezone_get_utc_timezone ();
1402 start = i_cal_time_new_from_timet_with_zone (dtstart_time, FALSE, utc_zone);
1403 end = i_cal_time_new_from_timet_with_zone (dtend_time, FALSE, utc_zone);
1404
1405 res = cb (e_cal_component_get_icalcomponent (comp), start, end, cb_data, NULL, NULL);
1406
1407 g_clear_object (&start);
1408 g_clear_object (&end);
1409
1410 return res;
1411 }
1412
1413 /*
1414 * Calls the given callback function for each occurrence of the given
1415 * recurrence rule between the given start and end times. If the rule is NULL
1416 * it uses all the rules from the component.
1417 *
1418 * If the callback routine returns FALSE the occurrence generation stops.
1419 *
1420 * The use of the specific rule is for determining the end of a rule when
1421 * COUNT is set. The callback will count instances and store the enddate
1422 * when COUNT is reached.
1423 *
1424 * Both start and end can be -1, in which case we start at the events first
1425 * instance and continue until it ends, or forever if it has no enddate.
1426 */
1427 static void
e_cal_recur_generate_instances_of_rule(ECalComponent * comp,ICalProperty * prop,time_t start,time_t end,ECalRecurInstanceCb cb,gpointer cb_data,ECalRecurResolveTimezoneCb tz_cb,gpointer tz_cb_data,ICalTimezone * default_timezone)1428 e_cal_recur_generate_instances_of_rule (ECalComponent *comp,
1429 ICalProperty *prop,
1430 time_t start,
1431 time_t end,
1432 ECalRecurInstanceCb cb,
1433 gpointer cb_data,
1434 ECalRecurResolveTimezoneCb tz_cb,
1435 gpointer tz_cb_data,
1436 ICalTimezone *default_timezone)
1437 {
1438 ECalComponentDateTime *dtstart, *dtend;
1439 time_t dtstart_time, dtend_time;
1440 GSList *rrules = NULL, *rdates = NULL;
1441 GSList *exrules = NULL, *exdates = NULL;
1442 CalObjTime interval_start, interval_end, event_start, event_end;
1443 CalObjTime chunk_start, chunk_end;
1444 gint days, seconds, year;
1445 gboolean single_rule, convert_end_date = FALSE;
1446 ICalTimezone *start_zone = NULL, *end_zone = NULL;
1447 ICalTime *dtstarttt, *dtendtt;
1448
1449 g_return_if_fail (comp != NULL);
1450 g_return_if_fail (cb != NULL);
1451 g_return_if_fail (tz_cb != NULL);
1452 g_return_if_fail (start >= -1);
1453 g_return_if_fail (end >= -1);
1454
1455 /* Get dtstart, dtend, recurrences, and exceptions. Note that
1456 * cal_component_get_dtend () will convert a DURATION property to a
1457 * DTEND so we don't need to worry about that. */
1458
1459 dtstart = e_cal_component_get_dtstart (comp);
1460 dtend = e_cal_component_get_dtend (comp);
1461
1462 dtstarttt = (dtstart && e_cal_component_datetime_get_value (dtstart)) ?
1463 e_cal_component_datetime_get_value (dtstart) : NULL;
1464 dtendtt = (dtend && e_cal_component_datetime_get_value (dtend)) ?
1465 e_cal_component_datetime_get_value (dtend) : NULL;
1466
1467 if (!dtstart || !dtstarttt) {
1468 g_message (
1469 "e_cal_recur_generate_instances_of_rule(): bogus "
1470 "component, does not have DTSTART. Skipping...");
1471 goto out;
1472 }
1473
1474 /* For DATE-TIME values with a TZID, we use the supplied callback to
1475 * resolve the TZID. For DATE values and DATE-TIME values without a
1476 * TZID (i.e. floating times) we use the default timezone. */
1477 if (e_cal_component_datetime_get_tzid (dtstart) && !i_cal_time_is_date (dtstarttt)) {
1478 start_zone = (*tz_cb) (e_cal_component_datetime_get_tzid (dtstart), tz_cb_data, NULL, NULL);
1479 if (!start_zone)
1480 start_zone = default_timezone;
1481 } else {
1482 start_zone = default_timezone;
1483
1484 /* Flag that we need to convert the saved ENDDATE property
1485 * to the default timezone. */
1486 convert_end_date = TRUE;
1487 }
1488
1489 if (start_zone)
1490 g_object_ref (start_zone);
1491
1492 dtstart_time = i_cal_time_as_timet_with_zone (dtstarttt, start_zone);
1493 if (start == -1)
1494 start = dtstart_time;
1495
1496 if (dtendtt) {
1497 /* If both DTSTART and DTEND are DATE values, and they are the
1498 * same day, we add 1 day to DTEND. This means that most
1499 * events created with the old Evolution behavior will still
1500 * work OK. I'm not sure what Outlook does in this case. */
1501 if (i_cal_time_is_date (dtstarttt) && i_cal_time_is_date (dtendtt)) {
1502 if (i_cal_time_compare_date_only (dtstarttt, dtendtt) == 0) {
1503 i_cal_time_adjust (dtendtt, 1, 0, 0, 0);
1504 }
1505 }
1506 } else {
1507 /* If there is no DTEND, then if DTSTART is a DATE-TIME value
1508 * we use the same time (so we have a single point in time).
1509 * If DTSTART is a DATE value we add 1 day. */
1510 e_cal_component_datetime_free (dtend);
1511
1512 dtend = e_cal_component_datetime_copy (dtstart);
1513 dtendtt = (dtend && e_cal_component_datetime_get_value (dtend)) ?
1514 e_cal_component_datetime_get_value (dtend) : NULL;
1515
1516 g_warn_if_fail (dtendtt != NULL);
1517
1518 if (i_cal_time_is_date (dtstarttt)) {
1519 i_cal_time_adjust (dtendtt, 1, 0, 0, 0);
1520 }
1521 }
1522
1523 if (e_cal_component_datetime_get_tzid (dtend) && dtendtt && !i_cal_time_is_date (dtendtt)) {
1524 end_zone = (*tz_cb) (e_cal_component_datetime_get_tzid (dtend), tz_cb_data, NULL, NULL);
1525 if (!end_zone)
1526 end_zone = default_timezone;
1527 } else {
1528 end_zone = default_timezone;
1529 }
1530
1531 if (end_zone)
1532 g_object_ref (end_zone);
1533
1534 /* We don't do this any more, since Outlook assumes that the DTEND
1535 * date is not included. */
1536 #if 0
1537 /* If DTEND is a DATE value, we add 1 day to it so that it includes
1538 * the entire day. */
1539 if (i_cal_time_is_date (dtendtt)) {
1540 i_cal_time_set_time (dtendtt, 0, 0, 0);
1541 i_cal_time_adjust (dtendtt, 1, 0, 0, 0);
1542 }
1543 #endif
1544 dtend_time = i_cal_time_as_timet_with_zone (dtendtt, end_zone);
1545
1546 /* If there is no recurrence, just call the callback if the event
1547 * intersects the given interval. */
1548 if (!(e_cal_component_has_recurrences (comp)
1549 || e_cal_component_has_exceptions (comp))) {
1550 if (e_cal_component_get_vtype (comp) == E_CAL_COMPONENT_JOURNAL) {
1551 ICalTime *start_t = i_cal_time_new_from_timet_with_zone (start, FALSE, default_timezone);
1552 ICalTime *end_t = i_cal_time_new_from_timet_with_zone (end, FALSE, default_timezone);
1553
1554 if ((i_cal_time_compare_date_only (dtstarttt, start_t) >= 0) && ((i_cal_time_compare_date_only (dtstarttt, end_t) < 0)))
1555 call_instance_callback (cb, comp, dtstart_time, dtend_time, cb_data);
1556
1557 g_clear_object (&start_t);
1558 g_clear_object (&end_t);
1559 } else if ((end == -1 || dtstart_time < end) && dtend_time > start) {
1560 call_instance_callback (cb, comp, dtstart_time, dtend_time, cb_data);
1561 }
1562
1563 goto out;
1564 }
1565
1566 /* If a specific recurrence rule is being used, set up a simple list,
1567 * else get the recurrence rules from the component. */
1568 if (prop) {
1569 single_rule = TRUE;
1570
1571 rrules = g_slist_prepend (NULL, g_object_ref (prop));
1572 } else if (e_cal_component_is_instance (comp)) {
1573 single_rule = FALSE;
1574 } else {
1575 single_rule = FALSE;
1576
1577 /* Make sure all the enddates for the rules are set. */
1578 e_cal_recur_ensure_end_dates (comp, FALSE, tz_cb, tz_cb_data, NULL, NULL);
1579
1580 rrules = e_cal_component_get_rrule_properties (comp);
1581 rdates = e_cal_component_get_rdates (comp);
1582 exrules = e_cal_component_get_exrule_properties (comp);
1583 exdates = e_cal_component_get_exdates (comp);
1584 }
1585
1586 /* Convert the interval start & end to CalObjTime. Note that if end
1587 * is -1 interval_end won't be set, so don't use it!
1588 * Also note that we use end - 1 since we want the interval to be
1589 * inclusive as it makes the code simpler. We do all calculation
1590 * in the timezone of the DTSTART. */
1591 cal_object_time_from_time (&interval_start, start, start_zone);
1592 if (end != -1)
1593 cal_object_time_from_time (&interval_end, end - 1, start_zone);
1594
1595 cal_object_time_from_time (&event_start, dtstart_time, start_zone);
1596 cal_object_time_from_time (&event_end, dtend_time, start_zone);
1597
1598 /* Calculate the duration of the event, which we use for all
1599 * occurrences. We can't just subtract start from end since that may
1600 * be affected by daylight-saving time. So we want a value of days
1601 * + seconds. */
1602 cal_object_compute_duration (
1603 &event_start, &event_end,
1604 &days, &seconds);
1605
1606 /* Take off the duration from interval_start, so we get occurrences
1607 * that start just before the start time but overlap it. But only do
1608 * that if the interval is after the event's start time. */
1609 if (start > dtstart_time) {
1610 cal_obj_time_add_days (&interval_start, -days);
1611 cal_obj_time_add_seconds (&interval_start, -seconds);
1612 }
1613
1614 /* Expand the recurrence for each year between start & end, or until
1615 * the callback returns 0 if end is 0. We do a year at a time to
1616 * give the callback function a chance to break out of the loop, and
1617 * so we don't get into problems with infinite recurrences. Since we
1618 * have to work on complete sets of occurrences, if there is a yearly
1619 * frequency it wouldn't make sense to break it into smaller chunks,
1620 * since we would then be calculating the same sets several times.
1621 * Though this does mean that we sometimes do a lot more work than
1622 * is necessary, e.g. if COUNT is set to something quite low. */
1623 for (year = interval_start.year;
1624 (end == -1 || year <= interval_end.year) && year <= MAX_YEAR;
1625 year++) {
1626 chunk_start = interval_start;
1627 chunk_start.year = year;
1628 if (end != -1)
1629 chunk_end = interval_end;
1630 chunk_end.year = year;
1631
1632 if (year != interval_start.year) {
1633 chunk_start.month = 0;
1634 chunk_start.day = 1;
1635 chunk_start.hour = 0;
1636 chunk_start.minute = 0;
1637 chunk_start.second = 0;
1638 }
1639 if (end == -1 || year != interval_end.year) {
1640 chunk_end.month = 11;
1641 chunk_end.day = 31;
1642 chunk_end.hour = 23;
1643 chunk_end.minute = 59;
1644 chunk_end.second = 61;
1645 chunk_end.flags = FALSE;
1646 }
1647
1648 if (!generate_instances_for_chunk (comp, dtstart_time,
1649 start_zone,
1650 rrules, rdates,
1651 exrules, exdates,
1652 single_rule,
1653 &event_start,
1654 start,
1655 &chunk_start, &chunk_end,
1656 days, seconds,
1657 convert_end_date,
1658 cb, cb_data))
1659 break;
1660 }
1661
1662 g_slist_free_full (rdates, e_cal_component_period_free);
1663 g_slist_free_full (exdates, e_cal_component_datetime_free);
1664 g_slist_free_full (rrules, g_object_unref);
1665 g_slist_free_full (exrules, g_object_unref);
1666
1667 out:
1668 e_cal_component_datetime_free (dtstart);
1669 e_cal_component_datetime_free (dtend);
1670 g_clear_object (&start_zone);
1671 g_clear_object (&end_zone);
1672 }
1673
1674 /* Builds a list of GINT_TO_POINTER() elements out of a short array from an
1675 * ICalRecurrence array of gshort.
1676 */
1677 static GList *
array_to_list_and_free(GArray * array)1678 array_to_list_and_free (GArray *array)
1679 {
1680 GList *lst;
1681 gint ii;
1682
1683 if (!array)
1684 return NULL;
1685
1686 lst = NULL;
1687
1688 for (ii = 0; ii < array->len; ii++) {
1689 gshort value = g_array_index (array, gshort, ii);
1690
1691 if (value == I_CAL_RECURRENCE_ARRAY_MAX)
1692 break;
1693
1694 lst = g_list_prepend (lst, GINT_TO_POINTER ((gint) value));
1695 }
1696
1697 g_array_unref (array);
1698
1699 return g_list_reverse (lst);
1700 }
1701
1702 /**
1703 * e_cal_recur_get_enddate:
1704 * @ir: RRULE or EXRULE recurrence
1705 * @prop: An RRULE or EXRULE #ICalProperty.
1706 * @zone: The DTSTART timezone, used for converting the UNTIL property if it
1707 * is given as a DATE value.
1708 * @convert_end_date: TRUE if the saved end date needs to be converted to the
1709 * given @zone timezone. This is needed if the DTSTART is a DATE or floating
1710 * time.
1711 *
1712 * Finds out end time of (reccurent) event.
1713 *
1714 * Returns: End timepoint of given event
1715 *
1716 * Since: 2.32
1717 */
1718 time_t
e_cal_recur_obtain_enddate(ICalRecurrence * ir,ICalProperty * prop,ICalTimezone * zone,gboolean convert_end_date)1719 e_cal_recur_obtain_enddate (ICalRecurrence *ir,
1720 ICalProperty *prop,
1721 ICalTimezone *zone,
1722 gboolean convert_end_date)
1723 {
1724 time_t enddate = -1;
1725
1726 g_return_val_if_fail (prop != NULL, 0);
1727 g_return_val_if_fail (ir != NULL, 0);
1728
1729 if (i_cal_recurrence_get_count (ir) != 0) {
1730 /* If COUNT is set, we use the pre-calculated enddate.
1731 Note that this can be 0 if the RULE doesn't actually
1732 generate COUNT instances. */
1733 enddate = e_cal_recur_get_rule_end_date (prop, convert_end_date ? zone : NULL);
1734 } else {
1735 ICalTime *until;
1736
1737 until = i_cal_recurrence_get_until (ir);
1738
1739 if (!until || i_cal_time_is_null_time (until)) {
1740 /* If neither COUNT or UNTIL is set, the event
1741 recurs forever. */
1742 } else if (i_cal_time_is_date (until)) {
1743 /* If UNTIL is a DATE, we stop at the end of
1744 the day, in local time (with the DTSTART timezone).
1745 Note that UNTIL is inclusive so we stop before
1746 midnight. */
1747 i_cal_time_set_time (until, 23, 59, 59);
1748 i_cal_time_set_is_date (until, FALSE);
1749
1750 enddate = i_cal_time_as_timet_with_zone (until, zone);
1751 } else {
1752 /* If UNTIL is a DATE-TIME, it must be in UTC. */
1753 enddate = i_cal_time_as_timet_with_zone (until, i_cal_timezone_get_utc_timezone ());
1754 }
1755
1756 g_clear_object (&until);
1757 }
1758
1759 return enddate;
1760 }
1761
1762 /*
1763 * e_cal_recur_from_icalproperty:
1764 * @prop: An RRULE or EXRULE #ICalProperty.
1765 * @exception: TRUE if this is an EXRULE rather than an RRULE.
1766 * @zone: The DTSTART timezone, used for converting the UNTIL property if it
1767 * is given as a DATE value.
1768 * @convert_end_date: TRUE if the saved end date needs to be converted to the
1769 * given @zone timezone. This is needed if the DTSTART is a DATE or floating
1770 * time.
1771 *
1772 * Converts an #ICalProperty to an #ECalRecurrence. This should be
1773 * freed using the e_cal_recur_free() function.
1774 *
1775 * Returns: #ECalRecurrence structure.
1776 */
1777 static ECalRecurrence *
e_cal_recur_from_icalproperty(ICalProperty * prop,gboolean exception,ICalTimezone * zone,gboolean convert_end_date)1778 e_cal_recur_from_icalproperty (ICalProperty *prop,
1779 gboolean exception,
1780 ICalTimezone *zone,
1781 gboolean convert_end_date)
1782 {
1783 ICalRecurrence *ir;
1784 ECalRecurrence *r;
1785 GArray *array;
1786 gint ii;
1787 GList *elem;
1788
1789 g_return_val_if_fail (prop != NULL, NULL);
1790
1791 r = g_slice_new0 (ECalRecurrence);
1792
1793 if (exception) {
1794 ir = i_cal_property_get_exrule (prop);
1795 } else {
1796 ir = i_cal_property_get_rrule (prop);
1797 }
1798
1799 r->freq = i_cal_recurrence_get_freq (ir);
1800
1801 if (G_UNLIKELY (i_cal_recurrence_get_interval (ir) < 1)) {
1802 gchar *str;
1803
1804 str = i_cal_recurrence_to_string (ir);
1805 g_warning (
1806 "Invalid interval in rule %s - using 1\n",
1807 str);
1808 g_free (str);
1809 r->interval = 1;
1810 } else {
1811 r->interval = i_cal_recurrence_get_interval (ir);
1812 }
1813
1814 r->enddate = e_cal_recur_obtain_enddate (ir, prop, zone, convert_end_date);
1815 r->week_start_day = e_cal_recur_ical_weekday_to_weekday (i_cal_recurrence_get_week_start (ir));
1816
1817 r->bymonth = array_to_list_and_free (i_cal_recurrence_get_by_month_array (ir));
1818 for (elem = r->bymonth; elem; elem = elem->next) {
1819 /* We need to convert from 1-12 to 0-11, i.e. subtract 1. */
1820 gint month = GPOINTER_TO_INT (elem->data) - 1;
1821 elem->data = GINT_TO_POINTER (month);
1822 }
1823
1824 r->byweekno = array_to_list_and_free (i_cal_recurrence_get_by_week_no_array (ir));
1825
1826 r->byyearday = array_to_list_and_free (i_cal_recurrence_get_by_year_day_array (ir));
1827
1828 r->bymonthday = array_to_list_and_free (i_cal_recurrence_get_by_month_day_array (ir));
1829
1830 /* FIXME: libical only supports 8 values, out of possible 107 * 7. */
1831 r->byday = NULL;
1832 array = i_cal_recurrence_get_by_day_array (ir);
1833 for (ii = 0; array && ii < array->len; ii++) {
1834 gshort value = g_array_index (array, gshort, ii);
1835 ICalRecurrenceWeekday day;
1836 gint weeknum, weekday;
1837
1838 if (value == I_CAL_RECURRENCE_ARRAY_MAX)
1839 break;
1840
1841 day = i_cal_recurrence_day_day_of_week (value);
1842 weeknum = i_cal_recurrence_day_position (value);
1843 weekday = e_cal_recur_ical_weekday_to_weekday (day);
1844
1845 r->byday = g_list_prepend (
1846 r->byday,
1847 GINT_TO_POINTER (weeknum));
1848 r->byday = g_list_prepend (
1849 r->byday,
1850 GINT_TO_POINTER (weekday));
1851 }
1852 if (array)
1853 g_array_unref (array);
1854
1855 r->byhour = array_to_list_and_free (i_cal_recurrence_get_by_hour_array (ir));
1856
1857 r->byminute = array_to_list_and_free (i_cal_recurrence_get_by_minute_array (ir));
1858
1859 r->bysecond = array_to_list_and_free (i_cal_recurrence_get_by_second_array (ir));
1860
1861 r->bysetpos = array_to_list_and_free (i_cal_recurrence_get_by_set_pos_array (ir));
1862
1863 g_clear_object (&ir);
1864
1865 return r;
1866 }
1867
1868 static gint
e_cal_recur_ical_weekday_to_weekday(ICalRecurrenceWeekday day)1869 e_cal_recur_ical_weekday_to_weekday (ICalRecurrenceWeekday day)
1870 {
1871 gint weekday;
1872
1873 switch (day) {
1874 case I_CAL_NO_WEEKDAY: /* Monday is the default in RFC2445. */
1875 case I_CAL_MONDAY_WEEKDAY:
1876 weekday = 0;
1877 break;
1878 case I_CAL_TUESDAY_WEEKDAY:
1879 weekday = 1;
1880 break;
1881 case I_CAL_WEDNESDAY_WEEKDAY:
1882 weekday = 2;
1883 break;
1884 case I_CAL_THURSDAY_WEEKDAY:
1885 weekday = 3;
1886 break;
1887 case I_CAL_FRIDAY_WEEKDAY:
1888 weekday = 4;
1889 break;
1890 case I_CAL_SATURDAY_WEEKDAY:
1891 weekday = 5;
1892 break;
1893 case I_CAL_SUNDAY_WEEKDAY:
1894 weekday = 6;
1895 break;
1896 default:
1897 g_warning (
1898 "e_cal_recur_ical_weekday_to_weekday(): Unknown week day %d",
1899 day);
1900 weekday = 0;
1901 }
1902
1903 return weekday;
1904 }
1905
1906 /**
1907 * e_cal_recur_free:
1908 * @r: An #ECalRecurrence structure.
1909 *
1910 * Frees an #ECalRecurrence structure.
1911 **/
1912 static void
e_cal_recur_free(ECalRecurrence * r)1913 e_cal_recur_free (ECalRecurrence *r)
1914 {
1915 g_return_if_fail (r != NULL);
1916
1917 g_list_free (r->bymonth);
1918 g_list_free (r->byweekno);
1919 g_list_free (r->byyearday);
1920 g_list_free (r->bymonthday);
1921 g_list_free (r->byday);
1922 g_list_free (r->byhour);
1923 g_list_free (r->byminute);
1924 g_list_free (r->bysecond);
1925 g_list_free (r->bysetpos);
1926
1927 g_slice_free (ECalRecurrence, r);
1928 }
1929
1930 /* Generates one year's worth of recurrence instances. Returns TRUE if all the
1931 * callback invocations returned TRUE, or FALSE when any one of them returns
1932 * FALSE, i.e. meaning that the instance generation should be stopped.
1933 *
1934 * This should only output instances whose start time is between chunk_start
1935 * and chunk_end (inclusive), or we may generate duplicates when we do the next
1936 * chunk. (This applies mainly to weekly recurrences, since weeks can span 2
1937 * years.)
1938 *
1939 * It should also only output instances that are on or after the event's
1940 * DTSTART property and that intersect the required interval, between
1941 * interval_start and interval_end.
1942 */
1943 static gboolean
generate_instances_for_chunk(ECalComponent * comp,time_t comp_dtstart,ICalTimezone * zone,GSList * rrules,GSList * rdates,GSList * exrules,GSList * exdates,gboolean single_rule,CalObjTime * event_start,time_t interval_start,CalObjTime * chunk_start,CalObjTime * chunk_end,gint duration_days,gint duration_seconds,gboolean convert_end_date,ECalRecurInstanceCb cb,gpointer cb_data)1944 generate_instances_for_chunk (ECalComponent *comp,
1945 time_t comp_dtstart,
1946 ICalTimezone *zone,
1947 GSList *rrules, /* ICalProperty * */
1948 GSList *rdates,
1949 GSList *exrules,
1950 GSList *exdates,
1951 gboolean single_rule,
1952 CalObjTime *event_start,
1953 time_t interval_start,
1954 CalObjTime *chunk_start,
1955 CalObjTime *chunk_end,
1956 gint duration_days,
1957 gint duration_seconds,
1958 gboolean convert_end_date,
1959 ECalRecurInstanceCb cb,
1960 gpointer cb_data)
1961 {
1962 GArray *occs, *ex_occs, *tmp_occs, *rdate_periods;
1963 CalObjTime cotime, *occ;
1964 GSList *elem;
1965 gint i;
1966 time_t start_time, end_time;
1967 gboolean cb_status = TRUE, rule_finished, finished = TRUE;
1968
1969 #if 0
1970 g_print (
1971 "In generate_instances_for_chunk rrules: %p\n"
1972 " %i/%i/%i %02i:%02i:%02i - %i/%i/%i %02i:%02i:%02i\n",
1973 rrules,
1974 chunk_start->day, chunk_start->month + 1,
1975 chunk_start->year, chunk_start->hour,
1976 chunk_start->minute, chunk_start->second,
1977 chunk_end->day, chunk_end->month + 1,
1978 chunk_end->year, chunk_end->hour,
1979 chunk_end->minute, chunk_end->second);
1980 #endif
1981
1982 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
1983 ex_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
1984 rdate_periods = g_array_new (
1985 FALSE, FALSE,
1986 sizeof (CalObjRecurrenceDate));
1987
1988 /* The original DTSTART property is included in the occurrence set,
1989 * but not if we are just generating occurrences for a single rule. */
1990 if (!single_rule) {
1991 /* We add it if it is in this chunk. If it is after this chunk
1992 * we set finished to FALSE, since we know we aren't finished
1993 * yet. */
1994 if (cal_obj_time_compare_func (event_start, chunk_end) >= 0)
1995 finished = FALSE;
1996 else if (cal_obj_time_compare_func (event_start, chunk_start) >= 0)
1997 g_array_append_vals (occs, event_start, 1);
1998 }
1999
2000 /* Expand each of the recurrence rules. */
2001 for (elem = rrules; elem; elem = elem->next) {
2002 ICalProperty *prop;
2003 ECalRecurrence *r;
2004
2005 prop = elem->data;
2006 r = e_cal_recur_from_icalproperty (
2007 prop, FALSE, zone,
2008 convert_end_date);
2009
2010 tmp_occs = cal_obj_expand_recurrence (
2011 event_start, zone, r,
2012 chunk_start,
2013 chunk_end,
2014 &rule_finished);
2015 e_cal_recur_free (r);
2016
2017 /* If any of the rules return FALSE for finished, we know we
2018 * have to carry on so we set finished to FALSE. */
2019 if (!rule_finished)
2020 finished = FALSE;
2021
2022 g_array_append_vals (occs, tmp_occs->data, tmp_occs->len);
2023 g_array_free (tmp_occs, TRUE);
2024 }
2025
2026 /* Add on specific RDATE occurrence dates. If they have an end time
2027 * or duration set, flag them as RDATEs, and store a pointer to the
2028 * period in the rdate_periods array. Otherwise we can just treat them
2029 * as normal occurrences. */
2030 for (elem = rdates; elem; elem = elem->next) {
2031 ECalComponentPeriod *period = elem->data;
2032 CalObjRecurrenceDate rdate;
2033 ICalTime *tt;
2034
2035 tt = e_cal_component_period_get_start (period);
2036 if (!tt)
2037 continue;
2038
2039 tt = i_cal_time_clone (tt);
2040 i_cal_time_convert_to_zone_inplace (tt, zone);
2041 cotime.year = i_cal_time_get_year (tt);
2042 cotime.month = i_cal_time_get_month (tt) - 1;
2043 cotime.day = i_cal_time_get_day (tt);
2044 cotime.hour = i_cal_time_get_hour (tt);
2045 cotime.minute = i_cal_time_get_minute (tt);
2046 cotime.second = i_cal_time_get_second (tt);
2047 cotime.flags = FALSE;
2048
2049 g_clear_object (&tt);
2050
2051 /* If the rdate is after the current chunk we set finished
2052 * to FALSE, and we skip it. */
2053 if (cal_obj_time_compare_func (&cotime, chunk_end) >= 0) {
2054 finished = FALSE;
2055 continue;
2056 }
2057
2058 /* Check if the end date or duration is set. If it is we need
2059 * to store it so we can get it later. (libical seems to set
2060 * second to -1 to denote an unset time. See icalvalue.c)
2061 * FIXME. */
2062 if (e_cal_component_period_get_kind (period) != E_CAL_COMPONENT_PERIOD_DATETIME ||
2063 !e_cal_component_period_get_end (period) ||
2064 i_cal_time_get_second (e_cal_component_period_get_end (period)) != -1) {
2065 cotime.flags = TRUE;
2066
2067 rdate.start = cotime;
2068 rdate.period = period;
2069 g_array_append_val (rdate_periods, rdate);
2070 }
2071
2072 g_array_append_val (occs, cotime);
2073 }
2074
2075 /* Expand each of the exception rules. */
2076 for (elem = exrules; elem; elem = elem->next) {
2077 ICalProperty *prop;
2078 ECalRecurrence *r;
2079
2080 prop = elem->data;
2081 r = e_cal_recur_from_icalproperty (
2082 prop, FALSE, zone,
2083 convert_end_date);
2084
2085 tmp_occs = cal_obj_expand_recurrence (
2086 event_start, zone, r,
2087 chunk_start,
2088 chunk_end,
2089 &rule_finished);
2090 e_cal_recur_free (r);
2091
2092 g_array_append_vals (ex_occs, tmp_occs->data, tmp_occs->len);
2093 g_array_free (tmp_occs, TRUE);
2094 }
2095
2096 /* Add on specific exception dates. */
2097 for (elem = exdates; elem; elem = elem->next) {
2098 ECalComponentDateTime *cdt;
2099 ICalTime *tt;
2100
2101 cdt = elem->data;
2102
2103 if (!e_cal_component_datetime_get_value (cdt))
2104 continue;
2105
2106 tt = e_cal_component_datetime_get_value (cdt);
2107 if (!tt)
2108 continue;
2109
2110 tt = i_cal_time_clone (tt);
2111 i_cal_time_convert_to_zone_inplace (tt, zone);
2112
2113 cotime.year = i_cal_time_get_year (tt);
2114 cotime.month = i_cal_time_get_month (tt) - 1;
2115 cotime.day = i_cal_time_get_day (tt);
2116
2117 /* If the EXDATE has a DATE value, set the time to the start
2118 * of the day and set flags to TRUE so we know to skip all
2119 * occurrences on that date. */
2120 if (i_cal_time_is_date (tt)) {
2121 cotime.hour = 0;
2122 cotime.minute = 0;
2123 cotime.second = 0;
2124 cotime.flags = TRUE;
2125 } else {
2126 cotime.hour = i_cal_time_get_hour (tt);
2127 cotime.minute = i_cal_time_get_minute (tt);
2128 cotime.second = i_cal_time_get_second (tt);
2129 cotime.flags = FALSE;
2130 }
2131
2132 g_array_append_val (ex_occs, cotime);
2133
2134 g_clear_object (&tt);
2135 }
2136
2137 /* Sort all the arrays. */
2138 cal_obj_sort_occurrences (occs);
2139 cal_obj_sort_occurrences (ex_occs);
2140
2141 if (rdate_periods->data && rdate_periods->len) {
2142 qsort (rdate_periods->data, rdate_periods->len,
2143 sizeof (CalObjRecurrenceDate), cal_obj_time_compare_func);
2144 }
2145
2146 /* Create the final array, by removing the exceptions from the
2147 * occurrences, and removing any duplicates. */
2148 cal_obj_remove_exceptions (occs, ex_occs);
2149
2150 /* Call the callback for each occurrence. If it returns 0 we break
2151 * out of the loop. */
2152 for (i = 0; i < occs->len; i++) {
2153 ICalTime *start_tt, *end_tt;
2154
2155 /* Convert each CalObjTime into a start & end time_t, and
2156 * check it is within the bounds of the event & interval. */
2157 occ = &g_array_index (occs, CalObjTime, i);
2158 #if 0
2159 g_print (
2160 "Checking occurrence: %s\n",
2161 cal_obj_time_to_string (occ));
2162 #endif
2163 start_tt = i_cal_time_new_null_time ();
2164 i_cal_time_set_date (start_tt, occ->year, occ->month + 1, occ->day);
2165 i_cal_time_set_time (start_tt, occ->hour, occ->minute, occ->second);
2166 start_time = i_cal_time_as_timet_with_zone (start_tt, zone);
2167 g_clear_object (&start_tt);
2168
2169 if (start_time == -1) {
2170 g_warning ("time_t out of range");
2171 finished = TRUE;
2172 break;
2173 }
2174
2175 /* Check to ensure that the start time is at or after the
2176 * event's DTSTART time, and that it is inside the chunk that
2177 * we are currently working on. (Note that the chunk_end time
2178 * is never after the interval end time, so this also tests
2179 * that we don't go past the end of the required interval). */
2180 if (start_time < comp_dtstart
2181 || cal_obj_time_compare_func (occ, chunk_start) < 0
2182 || cal_obj_time_compare_func (occ, chunk_end) > 0) {
2183 #if 0
2184 g_print (" start time invalid\n");
2185 #endif
2186 continue;
2187 }
2188
2189 if (occ->flags) {
2190 /* If it is an RDATE, we see if the end date or
2191 * duration was set. If not, we use the same duration
2192 * as the original occurrence. */
2193 if (!cal_object_get_rdate_end (occ, rdate_periods, zone)) {
2194 cal_obj_time_add_days (occ, duration_days);
2195 cal_obj_time_add_seconds (
2196 occ,
2197 duration_seconds);
2198 }
2199 } else {
2200 cal_obj_time_add_days (occ, duration_days);
2201 cal_obj_time_add_seconds (occ, duration_seconds);
2202 }
2203
2204 end_tt = i_cal_time_new_null_time ();
2205 i_cal_time_set_date (end_tt, occ->year, occ->month + 1, occ->day);
2206 i_cal_time_set_time (end_tt, occ->hour, occ->minute, occ->second);
2207 end_time = i_cal_time_as_timet_with_zone (end_tt, zone);
2208 g_clear_object (&end_tt);
2209
2210 if (end_time == -1) {
2211 g_warning ("time_t out of range");
2212 finished = TRUE;
2213 break;
2214 }
2215
2216 /* Check that the end time is after the interval start, so we
2217 * know that it intersects the required interval. */
2218 if (end_time <= interval_start) {
2219 #if 0
2220 g_print (" end time invalid\n");
2221 #endif
2222 continue;
2223 }
2224
2225 cb_status = call_instance_callback (cb, comp, start_time, end_time, cb_data);
2226 if (!cb_status)
2227 break;
2228 }
2229
2230 g_array_free (occs, TRUE);
2231 g_array_free (ex_occs, TRUE);
2232 g_array_free (rdate_periods, TRUE);
2233
2234 /* We return TRUE (i.e. carry on) only if the callback has always
2235 * returned TRUE and we know that we have more occurrences to generate
2236 * (i.e. finished is FALSE). */
2237 return cb_status && !finished;
2238 }
2239
2240 /* This looks up the occurrence time in the sorted rdate_periods array, and
2241 * tries to compute the end time of the occurrence. If no end time or duration
2242 * is set it returns FALSE and the default duration will be used. */
2243 static gboolean
cal_object_get_rdate_end(CalObjTime * occ,GArray * rdate_periods,ICalTimezone * zone)2244 cal_object_get_rdate_end (CalObjTime *occ,
2245 GArray *rdate_periods,
2246 ICalTimezone *zone)
2247 {
2248 CalObjRecurrenceDate *rdate = NULL;
2249 ECalComponentPeriod *period;
2250 gint lower, upper, middle, cmp = 0;
2251
2252 lower = 0;
2253 upper = rdate_periods->len;
2254
2255 while (lower < upper) {
2256 middle = (lower + upper) >> 1;
2257
2258 rdate = &g_array_index (rdate_periods, CalObjRecurrenceDate,
2259 middle);
2260
2261 cmp = cal_obj_time_compare_func (occ, &rdate->start);
2262
2263 if (cmp == 0)
2264 break;
2265 else if (cmp < 0)
2266 upper = middle;
2267 else
2268 lower = middle + 1;
2269 }
2270
2271 /* This should never happen. */
2272 if (cmp == 0 || !rdate) {
2273 #ifdef CAL_OBJ_DEBUG
2274 g_debug ("%s: Recurrence date %s not found", G_STRFUNC, cal_obj_time_to_string (cc));
2275 #endif
2276 return FALSE;
2277 }
2278
2279 period = rdate->period;
2280 if (e_cal_component_period_get_kind (period) == E_CAL_COMPONENT_PERIOD_DATETIME) {
2281 ICalTime *end;
2282
2283 end = e_cal_component_period_get_end (period);
2284 g_warn_if_fail (end != NULL);
2285 if (end) {
2286 ICalTime *tt;
2287
2288 tt = i_cal_time_convert_to_zone (end, zone);
2289
2290 occ->year = i_cal_time_get_year (tt);
2291 occ->month = i_cal_time_get_month (tt) - 1;
2292 occ->day = i_cal_time_get_day (tt);
2293 occ->hour = i_cal_time_get_hour (tt);
2294 occ->minute = i_cal_time_get_minute (tt);
2295 occ->second = i_cal_time_get_second (tt);
2296 occ->flags = FALSE;
2297
2298 g_clear_object (&tt);
2299 }
2300 } else {
2301 ICalDuration *duration;
2302
2303 duration = e_cal_component_period_get_duration (period);
2304
2305 cal_obj_time_add_days (
2306 occ,
2307 i_cal_duration_get_weeks (duration) * 7 +
2308 i_cal_duration_get_days (duration));
2309 cal_obj_time_add_hours (occ, i_cal_duration_get_hours (duration));
2310 cal_obj_time_add_minutes (occ, i_cal_duration_get_minutes (duration));
2311 cal_obj_time_add_seconds (occ, i_cal_duration_get_seconds (duration));
2312 }
2313
2314 return TRUE;
2315 }
2316
2317 static void
cal_object_compute_duration(CalObjTime * start,CalObjTime * end,gint * days,gint * seconds)2318 cal_object_compute_duration (CalObjTime *start,
2319 CalObjTime *end,
2320 gint *days,
2321 gint *seconds)
2322 {
2323 GDate start_date, end_date;
2324 gint start_seconds, end_seconds;
2325
2326 g_date_clear (&start_date, 1);
2327 g_date_clear (&end_date, 1);
2328 g_date_set_dmy (
2329 &start_date, start->day, start->month + 1, start->year);
2330 g_date_set_dmy (
2331 &end_date, end->day, end->month + 1, end->year);
2332
2333 *days = g_date_get_julian (&end_date) - g_date_get_julian (&start_date);
2334 start_seconds = start->hour * 3600 + start->minute * 60
2335 + start->second;
2336 end_seconds = end->hour * 3600 + end->minute * 60 + end->second;
2337
2338 *seconds = end_seconds - start_seconds;
2339 if (*seconds < 0) {
2340 *days = *days - 1;
2341 *seconds += 24 * 60 * 60;
2342 }
2343 }
2344
2345 /* Returns an unsorted GArray of CalObjTime's resulting from expanding the
2346 * given recurrence rule within the given interval. Note that it doesn't
2347 * clip the generated occurrences to the interval, i.e. if the interval
2348 * starts part way through the year this function still returns all the
2349 * occurrences for the year. Clipping is done later.
2350 * The finished flag is set to FALSE if there are more occurrences to generate
2351 * after the given interval.*/
2352 static GArray *
cal_obj_expand_recurrence(CalObjTime * event_start,ICalTimezone * zone,ECalRecurrence * recur,CalObjTime * interval_start,CalObjTime * interval_end,gboolean * finished)2353 cal_obj_expand_recurrence (CalObjTime *event_start,
2354 ICalTimezone *zone,
2355 ECalRecurrence *recur,
2356 CalObjTime *interval_start,
2357 CalObjTime *interval_end,
2358 gboolean *finished)
2359 {
2360 ECalRecurVTable vtable;
2361 CalObjTime *event_end = NULL, event_end_cotime;
2362 RecurData recur_data;
2363 CalObjTime occ, *cotime;
2364 GArray *all_occs, *occs;
2365 gint len;
2366
2367 /* This is the resulting array of CalObjTime elements. */
2368 all_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2369
2370 *finished = TRUE;
2371
2372 if (!cal_obj_get_vtable (recur, &vtable))
2373 return all_occs;
2374
2375 /* Calculate some useful data such as some fast lookup tables. */
2376 cal_obj_initialize_recur_data (&recur_data, recur, event_start);
2377
2378 /* Compute the event_end, if the recur's enddate is set. */
2379 if (recur->enddate > 0) {
2380 cal_object_time_from_time (
2381 &event_end_cotime,
2382 recur->enddate, zone);
2383 event_end = &event_end_cotime;
2384
2385 /* If the enddate is before the requested interval return. */
2386 if (cal_obj_time_compare_func (event_end, interval_start) < 0)
2387 return all_occs;
2388 }
2389
2390 /* Set finished to FALSE if we know there will be more occurrences to
2391 * do after this interval. */
2392 if (!interval_end || !event_end
2393 || cal_obj_time_compare_func (event_end, interval_end) > 0)
2394 *finished = FALSE;
2395
2396 /* Get the first period based on the frequency and the interval that
2397 * intersects the interval between start and end. */
2398 if ((*vtable.find_start_position) (event_start, event_end,
2399 &recur_data,
2400 interval_start, interval_end,
2401 &occ))
2402 return all_occs;
2403
2404 /* Loop until the event ends or we go past the end of the required
2405 * interval. */
2406 for (;;) {
2407 /* Generate the set of occurrences for this period. */
2408 switch (recur->freq) {
2409 case I_CAL_YEARLY_RECURRENCE:
2410 occs = cal_obj_generate_set_yearly (
2411 &recur_data,
2412 &vtable, &occ);
2413 break;
2414 case I_CAL_MONTHLY_RECURRENCE:
2415 occs = cal_obj_generate_set_monthly (
2416 &recur_data,
2417 &vtable, &occ);
2418 break;
2419 default:
2420 occs = cal_obj_generate_set_default (
2421 &recur_data,
2422 &vtable, &occ);
2423 break;
2424 }
2425
2426 /* Sort the occurrences and remove duplicates. */
2427 cal_obj_sort_occurrences (occs);
2428 cal_obj_remove_duplicates_and_invalid_dates (occs);
2429
2430 /* Apply the BYSETPOS property. */
2431 occs = cal_obj_bysetpos_filter (recur, occs);
2432
2433 /* Remove any occs after event_end. */
2434 len = occs->len - 1;
2435 if (event_end) {
2436 while (len >= 0) {
2437 cotime = &g_array_index (occs, CalObjTime,
2438 len);
2439 if (cal_obj_time_compare_func (cotime,
2440 event_end) <= 0)
2441 break;
2442 len--;
2443 }
2444 }
2445
2446 /* Add the occurrences onto the main array. */
2447 if (len >= 0)
2448 g_array_append_vals (all_occs, occs->data, len + 1);
2449
2450 g_array_free (occs, TRUE);
2451
2452 /* Skip to the next period, or exit the loop if finished. */
2453 if ((*vtable.find_next_position) (&occ, event_end,
2454 &recur_data, interval_end))
2455 break;
2456 }
2457
2458 return all_occs;
2459 }
2460
2461 static GArray *
cal_obj_generate_set_yearly(RecurData * recur_data,ECalRecurVTable * vtable,CalObjTime * occ)2462 cal_obj_generate_set_yearly (RecurData *recur_data,
2463 ECalRecurVTable *vtable,
2464 CalObjTime *occ)
2465 {
2466 ECalRecurrence *recur = recur_data->recur;
2467 GArray *occs_arrays[4], *occs, *occs2;
2468 gint num_occs_arrays = 0, i;
2469
2470 /* This is a bit complicated, since the iCalendar spec says that
2471 * several BYxxx modifiers can be used simultaneously. So we have to
2472 * be quite careful when determining the days of the occurrences.
2473 * The BYHOUR, BYMINUTE & BYSECOND modifiers are no problem at all.
2474 *
2475 * The modifiers we have to worry about are: BYMONTH, BYWEEKNO,
2476 * BYYEARDAY, BYMONTHDAY & BYDAY. We can't do these sequentially
2477 * since each filter will mess up the results of the previous one.
2478 * But they aren't all completely independant, e.g. BYMONTHDAY and
2479 * BYDAY are related to BYMONTH, and BYDAY is related to BYWEEKNO.
2480 *
2481 * BYDAY & BYMONTHDAY can also be applied independently, which makes
2482 * it worse. So we assume that if BYMONTH or BYWEEKNO is used, then
2483 * the BYDAY modifier applies to those, else it is applied
2484 * independantly.
2485 *
2486 * We expand the occurrences in parallel into the occs_arrays[] array,
2487 * and then merge them all into one GArray before expanding BYHOUR,
2488 * BYMINUTE & BYSECOND. */
2489
2490 if (recur->bymonth) {
2491 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2492 g_array_append_vals (occs, occ, 1);
2493
2494 occs = (*vtable->bymonth_filter) (recur_data, occs);
2495
2496 /* If BYMONTHDAY & BYDAY are both set we need to expand them
2497 * in parallel and add the results. */
2498 if (recur->bymonthday && recur->byday) {
2499 CalObjTime *prev_occ = NULL;
2500 GArray *new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2501
2502 /* Copy the occs array. */
2503 occs2 = g_array_new (
2504 FALSE, FALSE,
2505 sizeof (CalObjTime));
2506 g_array_append_vals (occs2, occs->data, occs->len);
2507
2508 occs = (*vtable->bymonthday_filter) (recur_data, occs);
2509 /* Note that we explicitly call the monthly version
2510 * of the BYDAY expansion filter. */
2511 occs2 = cal_obj_byday_expand_monthly (
2512 recur_data,
2513 occs2);
2514
2515 /* Add only intersection of those two arrays. */
2516 g_array_append_vals (occs, occs2->data, occs2->len);
2517 cal_obj_sort_occurrences (occs);
2518 for (i = 0; i < occs->len; i++) {
2519 CalObjTime *act_occ = &g_array_index (occs, CalObjTime, i);
2520
2521 if (prev_occ && cal_obj_time_compare_func (act_occ, prev_occ) == 0) {
2522 prev_occ = NULL;
2523 g_array_append_vals (new_occs, act_occ, 1);
2524 } else {
2525 prev_occ = act_occ;
2526 }
2527 }
2528
2529 g_array_free (occs, TRUE);
2530 g_array_free (occs2, TRUE);
2531
2532 occs = new_occs;
2533 } else {
2534 occs = (*vtable->bymonthday_filter) (recur_data, occs);
2535 /* Note that we explicitly call the monthly version
2536 * of the BYDAY expansion filter. */
2537 occs = cal_obj_byday_expand_monthly (recur_data, occs);
2538 }
2539
2540 occs_arrays[num_occs_arrays++] = occs;
2541 }
2542
2543 if (recur->byweekno) {
2544 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2545 g_array_append_vals (occs, occ, 1);
2546
2547 occs = (*vtable->byweekno_filter) (recur_data, occs);
2548 /* Note that we explicitly call the weekly version of the
2549 * BYDAY expansion filter. */
2550 occs = cal_obj_byday_expand_weekly (recur_data, occs);
2551
2552 occs_arrays[num_occs_arrays++] = occs;
2553 }
2554
2555 if (recur->byyearday) {
2556 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2557 g_array_append_vals (occs, occ, 1);
2558
2559 occs = (*vtable->byyearday_filter) (recur_data, occs);
2560
2561 occs_arrays[num_occs_arrays++] = occs;
2562 }
2563
2564 /* If BYMONTHDAY is set, and BYMONTH is not set, we need to
2565 * expand BYMONTHDAY independantly. */
2566 if (recur->bymonthday && !recur->bymonth) {
2567 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2568 g_array_append_vals (occs, occ, 1);
2569
2570 occs = (*vtable->bymonthday_filter) (recur_data, occs);
2571
2572 occs_arrays[num_occs_arrays++] = occs;
2573 }
2574
2575 /* If BYDAY is set, and BYMONTH and BYWEEKNO are not set, we need to
2576 * expand BYDAY independantly. */
2577 if (recur->byday && !recur->bymonth && !recur->byweekno) {
2578 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2579 g_array_append_vals (occs, occ, 1);
2580
2581 occs = (*vtable->byday_filter) (recur_data, occs);
2582
2583 occs_arrays[num_occs_arrays++] = occs;
2584 }
2585
2586 /* Add all the arrays together. If no filters were used we just
2587 * create an array with one element. */
2588 if (num_occs_arrays > 0) {
2589 occs = occs_arrays[0];
2590 for (i = 1; i < num_occs_arrays; i++) {
2591 occs2 = occs_arrays[i];
2592 g_array_append_vals (occs, occs2->data, occs2->len);
2593 g_array_free (occs2, TRUE);
2594 }
2595 } else {
2596 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2597 g_array_append_vals (occs, occ, 1);
2598 }
2599
2600 /* Now expand BYHOUR, BYMINUTE & BYSECOND. */
2601 occs = (*vtable->byhour_filter) (recur_data, occs);
2602 occs = (*vtable->byminute_filter) (recur_data, occs);
2603 occs = (*vtable->bysecond_filter) (recur_data, occs);
2604
2605 return occs;
2606 }
2607
2608 static GArray *
cal_obj_generate_set_monthly(RecurData * recur_data,ECalRecurVTable * vtable,CalObjTime * occ)2609 cal_obj_generate_set_monthly (RecurData *recur_data,
2610 ECalRecurVTable *vtable,
2611 CalObjTime *occ)
2612 {
2613 GArray *occs;
2614
2615 /* We start with just the one time in each set. */
2616 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2617 g_array_append_vals (occs, occ, 1);
2618
2619 occs = (*vtable->bymonth_filter) (recur_data, occs);
2620
2621 /* We need to combine the output of BYMONTHDAY & BYDAY, by doing them
2622 * in parallel rather than sequentially. If we did them sequentially
2623 * then we would lose the occurrences generated by BYMONTHDAY, and
2624 * instead have repetitions of the occurrences from BYDAY. */
2625 if (recur_data->recur->bymonthday && recur_data->recur->byday) {
2626 occs = (*vtable->byday_filter) (recur_data, occs);
2627 occs = (*vtable->bymonthday_filter) (recur_data, occs);
2628 } else {
2629 occs = (*vtable->bymonthday_filter) (recur_data, occs);
2630 occs = (*vtable->byday_filter) (recur_data, occs);
2631 }
2632
2633 occs = (*vtable->byhour_filter) (recur_data, occs);
2634 occs = (*vtable->byminute_filter) (recur_data, occs);
2635 occs = (*vtable->bysecond_filter) (recur_data, occs);
2636
2637 return occs;
2638 }
2639
2640 static GArray *
cal_obj_generate_set_default(RecurData * recur_data,ECalRecurVTable * vtable,CalObjTime * occ)2641 cal_obj_generate_set_default (RecurData *recur_data,
2642 ECalRecurVTable *vtable,
2643 CalObjTime *occ)
2644 {
2645 GArray *occs;
2646
2647 #if 0
2648 g_print (
2649 "Generating set for %i/%i/%i %02i:%02i:%02i\n",
2650 occ->day, occ->month + 1, occ->year, occ->hour, occ->minute,
2651 occ->second);
2652 #endif
2653
2654 /* We start with just the one time in the set. */
2655 occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2656 g_array_append_vals (occs, occ, 1);
2657
2658 occs = (*vtable->bymonth_filter) (recur_data, occs);
2659 if (vtable->byweekno_filter)
2660 occs = (*vtable->byweekno_filter) (recur_data, occs);
2661 if (vtable->byyearday_filter)
2662 occs = (*vtable->byyearday_filter) (recur_data, occs);
2663 if (vtable->bymonthday_filter)
2664 occs = (*vtable->bymonthday_filter) (recur_data, occs);
2665 occs = (*vtable->byday_filter) (recur_data, occs);
2666
2667 occs = (*vtable->byhour_filter) (recur_data, occs);
2668 occs = (*vtable->byminute_filter) (recur_data, occs);
2669 occs = (*vtable->bysecond_filter) (recur_data, occs);
2670
2671 return occs;
2672 }
2673
2674 /* Returns the function table corresponding to the recurrence frequency. */
2675 static gboolean
cal_obj_get_vtable(ECalRecurrence * recur,ECalRecurVTable * out_vtable)2676 cal_obj_get_vtable (ECalRecurrence *recur,
2677 ECalRecurVTable *out_vtable)
2678 {
2679 gboolean valid = TRUE;
2680
2681 switch (recur->freq) {
2682 case I_CAL_YEARLY_RECURRENCE:
2683 *out_vtable = cal_obj_yearly_vtable;
2684 break;
2685 case I_CAL_MONTHLY_RECURRENCE:
2686 *out_vtable = cal_obj_monthly_vtable;
2687 if (recur->bymonthday && recur->byday)
2688 out_vtable->bymonthday_filter = cal_obj_bymonthday_filter;
2689 else
2690 out_vtable->bymonthday_filter = cal_obj_bymonthday_expand;
2691 break;
2692 case I_CAL_WEEKLY_RECURRENCE:
2693 *out_vtable = cal_obj_weekly_vtable;
2694 break;
2695 case I_CAL_DAILY_RECURRENCE:
2696 *out_vtable = cal_obj_daily_vtable;
2697 break;
2698 case I_CAL_HOURLY_RECURRENCE:
2699 *out_vtable = cal_obj_hourly_vtable;
2700 break;
2701 case I_CAL_MINUTELY_RECURRENCE:
2702 *out_vtable = cal_obj_minutely_vtable;
2703 break;
2704 case I_CAL_SECONDLY_RECURRENCE:
2705 *out_vtable = cal_obj_secondly_vtable;
2706 break;
2707 default:
2708 g_warning ("Unknown recurrence frequency");
2709 valid = FALSE;
2710 }
2711
2712 return valid;
2713 }
2714
2715 /* This creates a number of fast lookup tables used when filtering with the
2716 * modifier properties BYMONTH, BYYEARDAY etc. */
2717 static void
cal_obj_initialize_recur_data(RecurData * recur_data,ECalRecurrence * recur,CalObjTime * event_start)2718 cal_obj_initialize_recur_data (RecurData *recur_data,
2719 ECalRecurrence *recur,
2720 CalObjTime *event_start)
2721 {
2722 GList *elem;
2723 gint month, yearday, monthday, weekday, hour, minute, second;
2724
2725 /* Clear the entire RecurData. */
2726 memset (recur_data, 0, sizeof (RecurData));
2727
2728 recur_data->recur = recur;
2729
2730 /* Set the weekday, used for the WEEKLY frequency and the BYWEEKNO
2731 * modifier. */
2732 recur_data->weekday_offset = cal_obj_time_weekday_offset (
2733 event_start,
2734 recur);
2735
2736 /* Create an array of months from bymonths for fast lookup. */
2737 elem = recur->bymonth;
2738 while (elem) {
2739 month = GPOINTER_TO_INT (elem->data);
2740 recur_data->months[month] = 1;
2741 elem = elem->next;
2742 }
2743
2744 /* Create an array of yeardays from byyearday for fast lookup.
2745 * We create a second array to handle the negative values. The first
2746 * element there corresponds to the last day of the year. */
2747 elem = recur->byyearday;
2748 while (elem) {
2749 yearday = GPOINTER_TO_INT (elem->data);
2750 if (yearday >= 0)
2751 recur_data->yeardays[yearday] = 1;
2752 else
2753 recur_data->neg_yeardays[-yearday] = 1;
2754 elem = elem->next;
2755 }
2756
2757 /* Create an array of monthdays from bymonthday for fast lookup.
2758 * We create a second array to handle the negative values. The first
2759 * element there corresponds to the last day of the month. */
2760 elem = recur->bymonthday;
2761 while (elem) {
2762 monthday = GPOINTER_TO_INT (elem->data);
2763 if (monthday >= 0)
2764 recur_data->monthdays[monthday] = 1;
2765 else
2766 recur_data->neg_monthdays[-monthday] = 1;
2767 elem = elem->next;
2768 }
2769
2770 /* Create an array of weekdays from byday for fast lookup. */
2771 elem = recur->byday;
2772 while (elem) {
2773 weekday = GPOINTER_TO_INT (elem->data);
2774 recur_data->weekdays[weekday] = 1;
2775 elem = elem->next;
2776 /* the second element is week_num, skip it */
2777 if (elem)
2778 elem = elem->next;
2779 }
2780
2781 /* Create an array of hours from byhour for fast lookup. */
2782 elem = recur->byhour;
2783 while (elem) {
2784 hour = GPOINTER_TO_INT (elem->data);
2785 recur_data->hours[hour] = 1;
2786 elem = elem->next;
2787 }
2788
2789 /* Create an array of minutes from byminutes for fast lookup. */
2790 elem = recur->byminute;
2791 while (elem) {
2792 minute = GPOINTER_TO_INT (elem->data);
2793 recur_data->minutes[minute] = 1;
2794 elem = elem->next;
2795 }
2796
2797 /* Create an array of seconds from byseconds for fast lookup. */
2798 elem = recur->bysecond;
2799 while (elem) {
2800 second = GPOINTER_TO_INT (elem->data);
2801 recur_data->seconds[second] = 1;
2802 elem = elem->next;
2803 }
2804 }
2805
2806 static void
cal_obj_sort_occurrences(GArray * occs)2807 cal_obj_sort_occurrences (GArray *occs)
2808 {
2809 if (occs->data && occs->len) {
2810 qsort (occs->data, occs->len, sizeof (CalObjTime),
2811 cal_obj_time_compare_func);
2812 }
2813 }
2814
2815 static void
cal_obj_remove_duplicates_and_invalid_dates(GArray * occs)2816 cal_obj_remove_duplicates_and_invalid_dates (GArray *occs)
2817 {
2818 static const gint days_in_month[12] = {
2819 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
2820 };
2821
2822 CalObjTime *occ, *prev_occ = NULL;
2823 gint len, i, j = 0, year, month, days;
2824 gboolean keep_occ;
2825
2826 len = occs->len;
2827 for (i = 0; i < len; i++) {
2828 occ = &g_array_index (occs, CalObjTime, i);
2829 keep_occ = TRUE;
2830
2831 if (prev_occ && cal_obj_time_compare_func (occ,
2832 prev_occ) == 0)
2833 keep_occ = FALSE;
2834
2835 year = occ->year;
2836 month = occ->month;
2837 days = days_in_month[occ->month];
2838 /* If it is february and a leap year, add a day. */
2839 if (month == 1 && (year % 4 == 0
2840 && (year % 100 != 0
2841 || year % 400 == 0)))
2842 days++;
2843
2844 if (occ->day > days) {
2845 /* move occurrence to the last day of the month */
2846 occ->day = days;
2847 }
2848
2849 if (keep_occ) {
2850 if (i != j)
2851 g_array_index (occs, CalObjTime, j)
2852 = g_array_index (occs, CalObjTime, i);
2853 j++;
2854 }
2855
2856 prev_occ = occ;
2857 }
2858
2859 g_array_set_size (occs, j);
2860 }
2861
2862 /* Removes the exceptions from the ex_occs array from the occurrences in the
2863 * occs array, and removes any duplicates. Both arrays are sorted. */
2864 static void
cal_obj_remove_exceptions(GArray * occs,GArray * ex_occs)2865 cal_obj_remove_exceptions (GArray *occs,
2866 GArray *ex_occs)
2867 {
2868 CalObjTime *occ, *prev_occ = NULL, *ex_occ = NULL, *last_occ_kept;
2869 gint i, j = 0, cmp, ex_index, occs_len, ex_occs_len;
2870 gboolean keep_occ, current_time_is_exception = FALSE;
2871
2872 if (occs->len == 0)
2873 return;
2874
2875 ex_index = 0;
2876 occs_len = occs->len;
2877 ex_occs_len = ex_occs->len;
2878
2879 if (ex_occs_len > 0)
2880 ex_occ = &g_array_index (ex_occs, CalObjTime, ex_index);
2881
2882 for (i = 0; i < occs_len; i++) {
2883 occ = &g_array_index (occs, CalObjTime, i);
2884 keep_occ = TRUE;
2885
2886 /* If the occurrence is a duplicate of the previous one, skip
2887 * it. */
2888 if (prev_occ
2889 && cal_obj_time_compare_func (occ, prev_occ) == 0) {
2890 keep_occ = FALSE;
2891
2892 /* If this occurrence is an RDATE with an end or
2893 * duration set, and the previous occurrence in the
2894 * array was kept, set the RDATE flag of the last one,
2895 * so we still use the end date or duration. */
2896 if (occ->flags && !current_time_is_exception) {
2897 last_occ_kept = &g_array_index (occs,
2898 CalObjTime,
2899 j - 1);
2900 last_occ_kept->flags = TRUE;
2901 }
2902 } else {
2903 /* We've found a new occurrence time. Reset the flag
2904 * to indicate that it hasn't been found in the
2905 * exceptions array (yet). */
2906 current_time_is_exception = FALSE;
2907
2908 if (ex_occ) {
2909 /* Step through the exceptions until we come
2910 * to one that matches or follows this
2911 * occurrence. */
2912 while (ex_occ) {
2913 /* If the exception is an EXDATE with
2914 * a DATE value, we only have to
2915 * compare the date. */
2916 if (ex_occ->flags)
2917 cmp = cal_obj_date_only_compare_func (ex_occ, occ);
2918 else
2919 cmp = cal_obj_time_compare_func (ex_occ, occ);
2920
2921 if (cmp > 0)
2922 break;
2923
2924 /* Move to the next exception, or set
2925 * ex_occ to NULL when we reach the
2926 * end of array. */
2927 ex_index++;
2928 if (ex_index < ex_occs_len)
2929 ex_occ = &g_array_index (ex_occs, CalObjTime, ex_index);
2930 else
2931 ex_occ = NULL;
2932
2933 /* If the exception did match this
2934 * occurrence we remove it, and set the
2935 * flag to indicate that the current
2936 * time is an exception. */
2937 if (cmp == 0) {
2938 current_time_is_exception = TRUE;
2939 keep_occ = FALSE;
2940 break;
2941 }
2942 }
2943 }
2944 }
2945
2946 if (keep_occ) {
2947 /* We are keeping this occurrence, so we move it to
2948 * the next free space, unless its position hasn't
2949 * changed (i.e. all previous occurrences were also
2950 * kept). */
2951 if (i != j)
2952 g_array_index (occs, CalObjTime, j)
2953 = g_array_index (occs, CalObjTime, i);
2954 j++;
2955 }
2956
2957 prev_occ = occ;
2958 }
2959
2960 g_array_set_size (occs, j);
2961 }
2962
2963 static GArray *
cal_obj_bysetpos_filter(ECalRecurrence * recur,GArray * occs)2964 cal_obj_bysetpos_filter (ECalRecurrence *recur,
2965 GArray *occs)
2966 {
2967 GArray *new_occs;
2968 CalObjTime *occ;
2969 GList *elem;
2970 gint len, pos;
2971
2972 /* If BYSETPOS has not been specified, or the array is empty, just
2973 * return the array. */
2974 elem = recur->bysetpos;
2975 if (!elem || occs->len == 0)
2976 return occs;
2977
2978 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
2979
2980 /* Iterate over the indices given in bysetpos, adding the corresponding
2981 * element from occs to new_occs. */
2982 len = occs->len;
2983 while (elem) {
2984 pos = GPOINTER_TO_INT (elem->data);
2985
2986 /* Negative values count back from the end of the array. */
2987 if (pos < 0)
2988 pos += len;
2989 /* Positive values need to be decremented since the array is
2990 * 0 - based. */
2991 else
2992 pos--;
2993
2994 if (pos >= 0 && pos < len) {
2995 occ = &g_array_index (occs, CalObjTime, pos);
2996 g_array_append_vals (new_occs, occ, 1);
2997 }
2998 elem = elem->next;
2999 }
3000
3001 g_array_free (occs, TRUE);
3002
3003 return new_occs;
3004 }
3005
3006 /* Finds the first year from the event_start, counting in multiples of the
3007 * recurrence interval, that intersects the given interval. It returns TRUE
3008 * if there is no intersection. */
3009 static gboolean
cal_obj_yearly_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3010 cal_obj_yearly_find_start_position (CalObjTime *event_start,
3011 CalObjTime *event_end,
3012 RecurData *recur_data,
3013 CalObjTime *interval_start,
3014 CalObjTime *interval_end,
3015 CalObjTime *cotime)
3016 {
3017 *cotime = *event_start;
3018
3019 /* Move on to the next interval, if the event starts before the
3020 * given interval. */
3021 if (cotime->year < interval_start->year) {
3022 gint years = interval_start->year - cotime->year
3023 + recur_data->recur->interval - 1;
3024 years -= years % recur_data->recur->interval;
3025 /* NOTE: The day may now be invalid, e.g. 29th Feb. */
3026 cotime->year += years;
3027 }
3028
3029 if ((event_end && cotime->year > event_end->year)
3030 || (interval_end && cotime->year > interval_end->year))
3031 return TRUE;
3032
3033 return FALSE;
3034 }
3035
3036 static gboolean
cal_obj_yearly_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3037 cal_obj_yearly_find_next_position (CalObjTime *cotime,
3038 CalObjTime *event_end,
3039 RecurData *recur_data,
3040 CalObjTime *interval_end)
3041 {
3042 /* NOTE: The day may now be invalid, e.g. 29th Feb. */
3043 cotime->year += recur_data->recur->interval;
3044
3045 if ((event_end && cotime->year > event_end->year)
3046 || (interval_end && cotime->year > interval_end->year))
3047 return TRUE;
3048
3049 return FALSE;
3050 }
3051
3052 static gboolean
cal_obj_monthly_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3053 cal_obj_monthly_find_start_position (CalObjTime *event_start,
3054 CalObjTime *event_end,
3055 RecurData *recur_data,
3056 CalObjTime *interval_start,
3057 CalObjTime *interval_end,
3058 CalObjTime *cotime)
3059 {
3060 *cotime = *event_start;
3061
3062 /* Move on to the next interval, if the event starts before the
3063 * given interval. */
3064 if (cal_obj_time_compare (cotime, interval_start, CALOBJ_MONTH) < 0) {
3065 gint months = (interval_start->year - cotime->year) * 12
3066 + interval_start->month - cotime->month
3067 + recur_data->recur->interval - 1;
3068 months -= months % recur_data->recur->interval;
3069 /* NOTE: The day may now be invalid, e.g. 31st Sep. */
3070 cal_obj_time_add_months (cotime, months);
3071 }
3072
3073 if (event_end && cal_obj_time_compare (cotime, event_end,
3074 CALOBJ_MONTH) > 0)
3075 return TRUE;
3076 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3077 CALOBJ_MONTH) > 0)
3078 return TRUE;
3079
3080 return FALSE;
3081 }
3082
3083 static gboolean
cal_obj_monthly_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3084 cal_obj_monthly_find_next_position (CalObjTime *cotime,
3085 CalObjTime *event_end,
3086 RecurData *recur_data,
3087 CalObjTime *interval_end)
3088 {
3089 /* NOTE: The day may now be invalid, e.g. 31st Sep. */
3090 cal_obj_time_add_months (cotime, recur_data->recur->interval);
3091
3092 if (event_end && cal_obj_time_compare (cotime, event_end,
3093 CALOBJ_MONTH) > 0)
3094 return TRUE;
3095 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3096 CALOBJ_MONTH) > 0)
3097 return TRUE;
3098
3099 return FALSE;
3100 }
3101
3102 static gboolean
cal_obj_weekly_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3103 cal_obj_weekly_find_start_position (CalObjTime *event_start,
3104 CalObjTime *event_end,
3105 RecurData *recur_data,
3106 CalObjTime *interval_start,
3107 CalObjTime *interval_end,
3108 CalObjTime *cotime)
3109 {
3110 GDate event_start_date, interval_start_date;
3111 guint32 event_start_julian, interval_start_julian;
3112 gint interval_start_weekday_offset;
3113 CalObjTime week_start;
3114
3115 if (event_end && cal_obj_time_compare (event_end, interval_start,
3116 CALOBJ_DAY) < 0)
3117 return TRUE;
3118 if (interval_end && cal_obj_time_compare (event_start, interval_end,
3119 CALOBJ_DAY) > 0)
3120 return TRUE;
3121
3122 *cotime = *event_start;
3123
3124 /* Convert the event start and interval start to GDates, so we can
3125 * easily find the number of days between them. */
3126 g_date_clear (&event_start_date, 1);
3127 g_date_set_dmy (
3128 &event_start_date, event_start->day,
3129 event_start->month + 1, event_start->year);
3130 g_date_clear (&interval_start_date, 1);
3131 g_date_set_dmy (
3132 &interval_start_date, interval_start->day,
3133 interval_start->month + 1, interval_start->year);
3134
3135 /* Calculate the start of the weeks corresponding to the event start
3136 * and interval start. */
3137 event_start_julian = g_date_get_julian (&event_start_date);
3138 event_start_julian -= recur_data->weekday_offset;
3139
3140 interval_start_julian = g_date_get_julian (&interval_start_date);
3141 interval_start_weekday_offset = cal_obj_time_weekday_offset (interval_start, recur_data->recur);
3142 interval_start_julian -= interval_start_weekday_offset;
3143
3144 /* We want to find the first full week using the recurrence interval
3145 * that intersects the given interval dates. */
3146 if (event_start_julian < interval_start_julian) {
3147 gint weeks = (interval_start_julian - event_start_julian) / 7;
3148 weeks += recur_data->recur->interval - 1;
3149 weeks -= weeks % recur_data->recur->interval;
3150 cal_obj_time_add_days (cotime, weeks * 7);
3151 }
3152
3153 week_start = *cotime;
3154 cal_obj_time_add_days (&week_start, -recur_data->weekday_offset);
3155
3156 if (event_end && cal_obj_time_compare (&week_start, event_end,
3157 CALOBJ_DAY) > 0)
3158 return TRUE;
3159 if (interval_end && cal_obj_time_compare (&week_start, interval_end,
3160 CALOBJ_DAY) > 0)
3161 return TRUE;
3162
3163 return FALSE;
3164 }
3165
3166 static gboolean
cal_obj_weekly_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3167 cal_obj_weekly_find_next_position (CalObjTime *cotime,
3168 CalObjTime *event_end,
3169 RecurData *recur_data,
3170 CalObjTime *interval_end)
3171 {
3172 CalObjTime week_start;
3173
3174 cal_obj_time_add_days (cotime, recur_data->recur->interval * 7);
3175
3176 /* Return TRUE if the start of this week is after the event finishes
3177 * or is after the end of the required interval. */
3178 week_start = *cotime;
3179 cal_obj_time_add_days (&week_start, -recur_data->weekday_offset);
3180
3181 #ifdef CAL_OBJ_DEBUG
3182 g_print ("Next day: %s\n", cal_obj_time_to_string (cotime));
3183 g_print ("Week Start: %s\n", cal_obj_time_to_string (&week_start));
3184 #endif
3185
3186 if (event_end && cal_obj_time_compare (&week_start, event_end,
3187 CALOBJ_DAY) > 0)
3188 return TRUE;
3189 if (interval_end && cal_obj_time_compare (&week_start, interval_end,
3190 CALOBJ_DAY) > 0) {
3191 #ifdef CAL_OBJ_DEBUG
3192 g_print (
3193 "Interval end reached: %s\n",
3194 cal_obj_time_to_string (interval_end));
3195 #endif
3196 return TRUE;
3197 }
3198
3199 return FALSE;
3200 }
3201
3202 static gboolean
cal_obj_daily_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3203 cal_obj_daily_find_start_position (CalObjTime *event_start,
3204 CalObjTime *event_end,
3205 RecurData *recur_data,
3206 CalObjTime *interval_start,
3207 CalObjTime *interval_end,
3208 CalObjTime *cotime)
3209 {
3210 GDate event_start_date, interval_start_date;
3211 guint32 event_start_julian, interval_start_julian, days;
3212
3213 if (interval_end && cal_obj_time_compare (event_start, interval_end,
3214 CALOBJ_DAY) > 0)
3215 return TRUE;
3216 if (event_end && cal_obj_time_compare (event_end, interval_start,
3217 CALOBJ_DAY) < 0)
3218 return TRUE;
3219
3220 *cotime = *event_start;
3221
3222 /* Convert the event start and interval start to GDates, so we can
3223 * easily find the number of days between them. */
3224 g_date_clear (&event_start_date, 1);
3225 g_date_set_dmy (
3226 &event_start_date, event_start->day,
3227 event_start->month + 1, event_start->year);
3228 g_date_clear (&interval_start_date, 1);
3229 g_date_set_dmy (
3230 &interval_start_date, interval_start->day,
3231 interval_start->month + 1, interval_start->year);
3232
3233 event_start_julian = g_date_get_julian (&event_start_date);
3234 interval_start_julian = g_date_get_julian (&interval_start_date);
3235
3236 if (event_start_julian < interval_start_julian) {
3237 days = interval_start_julian - event_start_julian
3238 + recur_data->recur->interval - 1;
3239 days -= days % recur_data->recur->interval;
3240 cal_obj_time_add_days (cotime, days);
3241 }
3242
3243 if (event_end && cal_obj_time_compare (cotime, event_end, CALOBJ_DAY) > 0)
3244 return TRUE;
3245 if (interval_end && cal_obj_time_compare (cotime, interval_end, CALOBJ_DAY) > 0)
3246 return TRUE;
3247
3248 return FALSE;
3249 }
3250
3251 static gboolean
cal_obj_daily_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3252 cal_obj_daily_find_next_position (CalObjTime *cotime,
3253 CalObjTime *event_end,
3254 RecurData *recur_data,
3255 CalObjTime *interval_end)
3256 {
3257 cal_obj_time_add_days (cotime, recur_data->recur->interval);
3258
3259 if (event_end && cal_obj_time_compare (cotime, event_end,
3260 CALOBJ_DAY) > 0)
3261 return TRUE;
3262 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3263 CALOBJ_DAY) > 0)
3264 return TRUE;
3265
3266 return FALSE;
3267 }
3268
3269 static gboolean
cal_obj_hourly_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3270 cal_obj_hourly_find_start_position (CalObjTime *event_start,
3271 CalObjTime *event_end,
3272 RecurData *recur_data,
3273 CalObjTime *interval_start,
3274 CalObjTime *interval_end,
3275 CalObjTime *cotime)
3276 {
3277 GDate event_start_date, interval_start_date;
3278 guint32 event_start_julian, interval_start_julian, hours;
3279
3280 if (interval_end && cal_obj_time_compare (event_start, interval_end,
3281 CALOBJ_HOUR) > 0)
3282 return TRUE;
3283 if (event_end && cal_obj_time_compare (event_end, interval_start,
3284 CALOBJ_HOUR) < 0)
3285 return TRUE;
3286
3287 *cotime = *event_start;
3288
3289 if (cal_obj_time_compare (event_start, interval_start,
3290 CALOBJ_HOUR) < 0) {
3291 /* Convert the event start and interval start to GDates, so we
3292 * can easily find the number of days between them. */
3293 g_date_clear (&event_start_date, 1);
3294 g_date_set_dmy (
3295 &event_start_date, event_start->day,
3296 event_start->month + 1, event_start->year);
3297 g_date_clear (&interval_start_date, 1);
3298 g_date_set_dmy (
3299 &interval_start_date, interval_start->day,
3300 interval_start->month + 1,
3301 interval_start->year);
3302
3303 event_start_julian = g_date_get_julian (&event_start_date);
3304 interval_start_julian = g_date_get_julian (&interval_start_date);
3305
3306 hours = (interval_start_julian - event_start_julian) * 24;
3307 hours += interval_start->hour - event_start->hour;
3308 hours += recur_data->recur->interval - 1;
3309 hours -= hours % recur_data->recur->interval;
3310 cal_obj_time_add_hours (cotime, hours);
3311 }
3312
3313 if (event_end && cal_obj_time_compare (cotime, event_end,
3314 CALOBJ_HOUR) > 0)
3315 return TRUE;
3316 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3317 CALOBJ_HOUR) > 0)
3318 return TRUE;
3319
3320 return FALSE;
3321 }
3322
3323 static gboolean
cal_obj_hourly_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3324 cal_obj_hourly_find_next_position (CalObjTime *cotime,
3325 CalObjTime *event_end,
3326 RecurData *recur_data,
3327 CalObjTime *interval_end)
3328 {
3329 cal_obj_time_add_hours (cotime, recur_data->recur->interval);
3330
3331 if (event_end && cal_obj_time_compare (cotime, event_end,
3332 CALOBJ_HOUR) > 0)
3333 return TRUE;
3334 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3335 CALOBJ_HOUR) > 0)
3336 return TRUE;
3337
3338 return FALSE;
3339 }
3340
3341 static gboolean
cal_obj_minutely_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3342 cal_obj_minutely_find_start_position (CalObjTime *event_start,
3343 CalObjTime *event_end,
3344 RecurData *recur_data,
3345 CalObjTime *interval_start,
3346 CalObjTime *interval_end,
3347 CalObjTime *cotime)
3348 {
3349 GDate event_start_date, interval_start_date;
3350 guint32 event_start_julian, interval_start_julian, minutes;
3351
3352 if (interval_end && cal_obj_time_compare (event_start, interval_end,
3353 CALOBJ_MINUTE) > 0)
3354 return TRUE;
3355 if (event_end && cal_obj_time_compare (event_end, interval_start,
3356 CALOBJ_MINUTE) < 0)
3357 return TRUE;
3358
3359 *cotime = *event_start;
3360
3361 if (cal_obj_time_compare (event_start, interval_start,
3362 CALOBJ_MINUTE) < 0) {
3363 /* Convert the event start and interval start to GDates, so we
3364 * can easily find the number of days between them. */
3365 g_date_clear (&event_start_date, 1);
3366 g_date_set_dmy (
3367 &event_start_date, event_start->day,
3368 event_start->month + 1, event_start->year);
3369 g_date_clear (&interval_start_date, 1);
3370 g_date_set_dmy (
3371 &interval_start_date, interval_start->day,
3372 interval_start->month + 1,
3373 interval_start->year);
3374
3375 event_start_julian = g_date_get_julian (&event_start_date);
3376 interval_start_julian = g_date_get_julian (&interval_start_date);
3377
3378 minutes = (interval_start_julian - event_start_julian)
3379 * 24 * 60;
3380 minutes += (interval_start->hour - event_start->hour) * 24;
3381 minutes += interval_start->minute - event_start->minute;
3382 minutes += recur_data->recur->interval - 1;
3383 minutes -= minutes % recur_data->recur->interval;
3384 cal_obj_time_add_minutes (cotime, minutes);
3385 }
3386
3387 if (event_end && cal_obj_time_compare (cotime, event_end,
3388 CALOBJ_MINUTE) > 0)
3389 return TRUE;
3390 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3391 CALOBJ_MINUTE) > 0)
3392 return TRUE;
3393
3394 return FALSE;
3395 }
3396
3397 static gboolean
cal_obj_minutely_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3398 cal_obj_minutely_find_next_position (CalObjTime *cotime,
3399 CalObjTime *event_end,
3400 RecurData *recur_data,
3401 CalObjTime *interval_end)
3402 {
3403 cal_obj_time_add_minutes (cotime, recur_data->recur->interval);
3404
3405 if (event_end && cal_obj_time_compare (cotime, event_end,
3406 CALOBJ_MINUTE) > 0)
3407 return TRUE;
3408 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3409 CALOBJ_MINUTE) > 0)
3410 return TRUE;
3411
3412 return FALSE;
3413 }
3414
3415 static gboolean
cal_obj_secondly_find_start_position(CalObjTime * event_start,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_start,CalObjTime * interval_end,CalObjTime * cotime)3416 cal_obj_secondly_find_start_position (CalObjTime *event_start,
3417 CalObjTime *event_end,
3418 RecurData *recur_data,
3419 CalObjTime *interval_start,
3420 CalObjTime *interval_end,
3421 CalObjTime *cotime)
3422 {
3423 GDate event_start_date, interval_start_date;
3424 guint32 event_start_julian, interval_start_julian, seconds;
3425
3426 if (interval_end && cal_obj_time_compare (event_start, interval_end,
3427 CALOBJ_SECOND) > 0)
3428 return TRUE;
3429 if (event_end && cal_obj_time_compare (event_end, interval_start,
3430 CALOBJ_SECOND) < 0)
3431 return TRUE;
3432
3433 *cotime = *event_start;
3434
3435 if (cal_obj_time_compare (event_start, interval_start,
3436 CALOBJ_SECOND) < 0) {
3437 /* Convert the event start and interval start to GDates, so we
3438 * can easily find the number of days between them. */
3439 g_date_clear (&event_start_date, 1);
3440 g_date_set_dmy (
3441 &event_start_date, event_start->day,
3442 event_start->month + 1, event_start->year);
3443 g_date_clear (&interval_start_date, 1);
3444 g_date_set_dmy (
3445 &interval_start_date, interval_start->day,
3446 interval_start->month + 1,
3447 interval_start->year);
3448
3449 event_start_julian = g_date_get_julian (&event_start_date);
3450 interval_start_julian = g_date_get_julian (&interval_start_date);
3451
3452 seconds = (interval_start_julian - event_start_julian)
3453 * 24 * 60 * 60;
3454 seconds += (interval_start->hour - event_start->hour)
3455 * 24 * 60;
3456 seconds += (interval_start->minute - event_start->minute) * 60;
3457 seconds += interval_start->second - event_start->second;
3458 seconds += recur_data->recur->interval - 1;
3459 seconds -= seconds % recur_data->recur->interval;
3460 cal_obj_time_add_seconds (cotime, seconds);
3461 }
3462
3463 if (event_end && cal_obj_time_compare (cotime, event_end,
3464 CALOBJ_SECOND) >= 0)
3465 return TRUE;
3466 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3467 CALOBJ_SECOND) >= 0)
3468 return TRUE;
3469
3470 return FALSE;
3471 }
3472
3473 static gboolean
cal_obj_secondly_find_next_position(CalObjTime * cotime,CalObjTime * event_end,RecurData * recur_data,CalObjTime * interval_end)3474 cal_obj_secondly_find_next_position (CalObjTime *cotime,
3475 CalObjTime *event_end,
3476 RecurData *recur_data,
3477 CalObjTime *interval_end)
3478 {
3479 cal_obj_time_add_seconds (cotime, recur_data->recur->interval);
3480
3481 if (event_end && cal_obj_time_compare (cotime, event_end,
3482 CALOBJ_SECOND) >= 0)
3483 return TRUE;
3484 if (interval_end && cal_obj_time_compare (cotime, interval_end,
3485 CALOBJ_SECOND) >= 0)
3486 return TRUE;
3487
3488 return FALSE;
3489 }
3490
3491 /* If the BYMONTH rule is specified it expands each occurrence in occs, by
3492 * using each of the months in the bymonth list. */
3493 static GArray *
cal_obj_bymonth_expand(RecurData * recur_data,GArray * occs)3494 cal_obj_bymonth_expand (RecurData *recur_data,
3495 GArray *occs)
3496 {
3497 GArray *new_occs;
3498 CalObjTime *occ;
3499 GList *elem;
3500 gint len, i;
3501
3502 /* If BYMONTH has not been specified, or the array is empty, just
3503 * return the array. */
3504 if (!recur_data->recur->bymonth || occs->len == 0)
3505 return occs;
3506
3507 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3508
3509 len = occs->len;
3510 for (i = 0; i < len; i++) {
3511 occ = &g_array_index (occs, CalObjTime, i);
3512
3513 elem = recur_data->recur->bymonth;
3514 while (elem) {
3515 /* NOTE: The day may now be invalid, e.g. 31st Feb. */
3516 occ->month = GPOINTER_TO_INT (elem->data);
3517 g_array_append_vals (new_occs, occ, 1);
3518 elem = elem->next;
3519 }
3520 }
3521
3522 g_array_free (occs, TRUE);
3523
3524 return new_occs;
3525 }
3526
3527 /* If the BYMONTH rule is specified it filters out all occurrences in occs
3528 * which do not match one of the months in the bymonth list. */
3529 static GArray *
cal_obj_bymonth_filter(RecurData * recur_data,GArray * occs)3530 cal_obj_bymonth_filter (RecurData *recur_data,
3531 GArray *occs)
3532 {
3533 GArray *new_occs;
3534 CalObjTime *occ;
3535 gint len, i;
3536
3537 /* If BYMONTH has not been specified, or the array is empty, just
3538 * return the array. */
3539 if (!recur_data->recur->bymonth || occs->len == 0)
3540 return occs;
3541
3542 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3543
3544 len = occs->len;
3545 for (i = 0; i < len; i++) {
3546 occ = &g_array_index (occs, CalObjTime, i);
3547 if (recur_data->months[occ->month])
3548 g_array_append_vals (new_occs, occ, 1);
3549 }
3550
3551 g_array_free (occs, TRUE);
3552
3553 return new_occs;
3554 }
3555
3556 static GArray *
cal_obj_byweekno_expand(RecurData * recur_data,GArray * occs)3557 cal_obj_byweekno_expand (RecurData *recur_data,
3558 GArray *occs)
3559 {
3560 GArray *new_occs;
3561 CalObjTime *occ, year_start_cotime, year_end_cotime, cotime;
3562 GList *elem;
3563 gint len, i, weekno;
3564
3565 /* If BYWEEKNO has not been specified, or the array is empty, just
3566 * return the array. */
3567 if (!recur_data->recur->byweekno || occs->len == 0)
3568 return occs;
3569
3570 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3571
3572 len = occs->len;
3573 for (i = 0; i < len; i++) {
3574 occ = &g_array_index (occs, CalObjTime, i);
3575
3576 /* Find the day that would correspond to week 1 (note that
3577 * week 1 is the first week starting from the specified week
3578 * start day that has 4 days in the new year). */
3579 year_start_cotime = *occ;
3580 cal_obj_time_find_first_week (
3581 &year_start_cotime,
3582 recur_data);
3583
3584 /* Find the day that would correspond to week 1 of the next
3585 * year, which we use for -ve week numbers. */
3586 year_end_cotime = *occ;
3587 year_end_cotime.year++;
3588 cal_obj_time_find_first_week (
3589 &year_end_cotime,
3590 recur_data);
3591
3592 /* Now iterate over the week numbers in byweekno, generating a
3593 * new occurrence for each one. */
3594 elem = recur_data->recur->byweekno;
3595 while (elem) {
3596 weekno = GPOINTER_TO_INT (elem->data);
3597 if (weekno > 0) {
3598 cotime = year_start_cotime;
3599 cal_obj_time_add_days (
3600 &cotime,
3601 (weekno - 1) * 7);
3602 } else {
3603 cotime = year_end_cotime;
3604 cal_obj_time_add_days (&cotime, weekno * 7);
3605 }
3606
3607 /* Skip occurrences if they fall outside the year. */
3608 if (cotime.year == occ->year)
3609 g_array_append_val (new_occs, cotime);
3610 elem = elem->next;
3611 }
3612 }
3613
3614 g_array_free (occs, TRUE);
3615
3616 return new_occs;
3617 }
3618
3619 static GArray *
cal_obj_byyearday_expand(RecurData * recur_data,GArray * occs)3620 cal_obj_byyearday_expand (RecurData *recur_data,
3621 GArray *occs)
3622 {
3623 GArray *new_occs;
3624 CalObjTime *occ, year_start_cotime, year_end_cotime, cotime;
3625 GList *elem;
3626 gint len, i, dayno;
3627
3628 /* If BYYEARDAY has not been specified, or the array is empty, just
3629 * return the array. */
3630 if (!recur_data->recur->byyearday || occs->len == 0)
3631 return occs;
3632
3633 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3634
3635 len = occs->len;
3636 for (i = 0; i < len; i++) {
3637 occ = &g_array_index (occs, CalObjTime, i);
3638
3639 /* Find the day that would correspond to day 1. */
3640 year_start_cotime = *occ;
3641 year_start_cotime.month = 0;
3642 year_start_cotime.day = 1;
3643
3644 /* Find the day that would correspond to day 1 of the next
3645 * year, which we use for -ve day numbers. */
3646 year_end_cotime = *occ;
3647 year_end_cotime.year++;
3648 year_end_cotime.month = 0;
3649 year_end_cotime.day = 1;
3650
3651 /* Now iterate over the day numbers in byyearday, generating a
3652 * new occurrence for each one. */
3653 elem = recur_data->recur->byyearday;
3654 while (elem) {
3655 dayno = GPOINTER_TO_INT (elem->data);
3656 if (dayno > 0) {
3657 cotime = year_start_cotime;
3658 cal_obj_time_add_days (&cotime, dayno - 1);
3659 } else {
3660 cotime = year_end_cotime;
3661 cal_obj_time_add_days (&cotime, dayno);
3662 }
3663
3664 /* Skip occurrences if they fall outside the year. */
3665 if (cotime.year == occ->year)
3666 g_array_append_val (new_occs, cotime);
3667 elem = elem->next;
3668 }
3669 }
3670
3671 g_array_free (occs, TRUE);
3672
3673 return new_occs;
3674 }
3675
3676 /* Note: occs must not contain invalid dates, e.g. 31st September. */
3677 static GArray *
cal_obj_byyearday_filter(RecurData * recur_data,GArray * occs)3678 cal_obj_byyearday_filter (RecurData *recur_data,
3679 GArray *occs)
3680 {
3681 GArray *new_occs;
3682 CalObjTime *occ;
3683 gint yearday, len, i, days_in_year;
3684
3685 /* If BYYEARDAY has not been specified, or the array is empty, just
3686 * return the array. */
3687 if (!recur_data->recur->byyearday || occs->len == 0)
3688 return occs;
3689
3690 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3691
3692 len = occs->len;
3693 for (i = 0; i < len; i++) {
3694 occ = &g_array_index (occs, CalObjTime, i);
3695 yearday = cal_obj_time_day_of_year (occ);
3696 if (recur_data->yeardays[yearday]) {
3697 g_array_append_vals (new_occs, occ, 1);
3698 } else {
3699 days_in_year = g_date_is_leap_year (occ->year)
3700 ? 366 : 365;
3701 if (recur_data->neg_yeardays[days_in_year + 1
3702 - yearday])
3703 g_array_append_vals (new_occs, occ, 1);
3704 }
3705 }
3706
3707 g_array_free (occs, TRUE);
3708
3709 return new_occs;
3710 }
3711
3712 static GArray *
cal_obj_bymonthday_expand(RecurData * recur_data,GArray * occs)3713 cal_obj_bymonthday_expand (RecurData *recur_data,
3714 GArray *occs)
3715 {
3716 GArray *new_occs;
3717 CalObjTime *occ, month_start_cotime, month_end_cotime, cotime;
3718 GList *elem;
3719 gint len, i, dayno;
3720
3721 /* If BYMONTHDAY has not been specified, or the array is empty, just
3722 * return the array. */
3723 if (!recur_data->recur->bymonthday || occs->len == 0)
3724 return occs;
3725
3726 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3727
3728 len = occs->len;
3729 for (i = 0; i < len; i++) {
3730 occ = &g_array_index (occs, CalObjTime, i);
3731
3732 /* Find the day that would correspond to day 1. */
3733 month_start_cotime = *occ;
3734 month_start_cotime.day = 1;
3735
3736 /* Find the day that would correspond to day 1 of the next
3737 * month, which we use for -ve day numbers. */
3738 month_end_cotime = *occ;
3739 month_end_cotime.month++;
3740 month_end_cotime.day = 1;
3741
3742 /* Now iterate over the day numbers in bymonthday, generating a
3743 * new occurrence for each one. */
3744 elem = recur_data->recur->bymonthday;
3745 while (elem) {
3746 dayno = GPOINTER_TO_INT (elem->data);
3747 if (dayno > 0) {
3748 cotime = month_start_cotime;
3749 cal_obj_time_add_days (&cotime, dayno - 1);
3750 } else {
3751 cotime = month_end_cotime;
3752 cal_obj_time_add_days (&cotime, dayno);
3753 }
3754 if (cotime.month == occ->month) {
3755 g_array_append_val (new_occs, cotime);
3756 } else {
3757 /* set to last day in month */
3758 cotime.month = occ->month;
3759 cotime.day = time_days_in_month (occ->year, occ->month);
3760 g_array_append_val (new_occs, cotime);
3761 }
3762
3763 elem = elem->next;
3764 }
3765 }
3766
3767 g_array_free (occs, TRUE);
3768
3769 return new_occs;
3770 }
3771
3772 static GArray *
cal_obj_bymonthday_filter(RecurData * recur_data,GArray * occs)3773 cal_obj_bymonthday_filter (RecurData *recur_data,
3774 GArray *occs)
3775 {
3776 GArray *new_occs;
3777 CalObjTime *occ;
3778 gint len, i, days_in_month;
3779
3780 /* If BYMONTHDAY has not been specified, or the array is empty, just
3781 * return the array. */
3782 if (!recur_data->recur->bymonthday || occs->len == 0)
3783 return occs;
3784
3785 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3786
3787 len = occs->len;
3788 for (i = 0; i < len; i++) {
3789 occ = &g_array_index (occs, CalObjTime, i);
3790 if (recur_data->monthdays[occ->day]) {
3791 g_array_append_vals (new_occs, occ, 1);
3792 } else {
3793 days_in_month = time_days_in_month (
3794 occ->year,
3795 occ->month);
3796 if (recur_data->neg_monthdays[days_in_month + 1 - occ->day])
3797 g_array_append_vals (new_occs, occ, 1);
3798 }
3799 }
3800
3801 g_array_free (occs, TRUE);
3802
3803 return new_occs;
3804 }
3805
3806 static GArray *
cal_obj_byday_expand_yearly(RecurData * recur_data,GArray * occs)3807 cal_obj_byday_expand_yearly (RecurData *recur_data,
3808 GArray *occs)
3809 {
3810 GArray *new_occs;
3811 CalObjTime *occ;
3812 GList *elem;
3813 gint len, i, weekday, week_num;
3814 gint first_weekday, last_weekday, offset;
3815 guint16 year;
3816
3817 /* If BYDAY has not been specified, or the array is empty, just
3818 * return the array. */
3819 if (!recur_data->recur->byday || occs->len == 0)
3820 return occs;
3821
3822 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3823
3824 len = occs->len;
3825 for (i = 0; i < len; i++) {
3826 occ = &g_array_index (occs, CalObjTime, i);
3827
3828 elem = recur_data->recur->byday;
3829 while (elem) {
3830 weekday = GPOINTER_TO_INT (elem->data);
3831 elem = elem->next;
3832 week_num = GPOINTER_TO_INT (elem->data);
3833 elem = elem->next;
3834
3835 year = occ->year;
3836 if (week_num == 0) {
3837 /* Expand to every Mon/Tue/etc. in the year. */
3838 occ->month = 0;
3839 occ->day = 1;
3840 first_weekday = cal_obj_time_weekday (occ);
3841 offset = (weekday + 7 - first_weekday) % 7;
3842 cal_obj_time_add_days (occ, offset);
3843
3844 while (occ->year == year) {
3845 g_array_append_vals (new_occs, occ, 1);
3846 cal_obj_time_add_days (occ, 7);
3847 }
3848
3849 } else if (week_num > 0) {
3850 /* Add the nth Mon/Tue/etc. in the year. */
3851 occ->month = 0;
3852 occ->day = 1;
3853 first_weekday = cal_obj_time_weekday (occ);
3854 offset = (weekday + 7 - first_weekday) % 7;
3855 offset += (week_num - 1) * 7;
3856 cal_obj_time_add_days (occ, offset);
3857 if (occ->year == year)
3858 g_array_append_vals (new_occs, occ, 1);
3859
3860 } else {
3861 /* Add the -nth Mon/Tue/etc. in the year. */
3862 occ->month = 11;
3863 occ->day = 31;
3864 last_weekday = cal_obj_time_weekday (occ);
3865 offset = (last_weekday + 7 - weekday) % 7;
3866 offset += (week_num - 1) * 7;
3867 cal_obj_time_add_days (occ, -offset);
3868 if (occ->year == year)
3869 g_array_append_vals (new_occs, occ, 1);
3870 }
3871
3872 /* Reset the year, as we may have gone past the end. */
3873 occ->year = year;
3874 }
3875 }
3876
3877 g_array_free (occs, TRUE);
3878
3879 return new_occs;
3880 }
3881
3882 static GArray *
cal_obj_byday_expand_monthly(RecurData * recur_data,GArray * occs)3883 cal_obj_byday_expand_monthly (RecurData *recur_data,
3884 GArray *occs)
3885 {
3886 GArray *new_occs;
3887 CalObjTime *occ;
3888 GList *elem;
3889 gint len, i, weekday, week_num;
3890 gint first_weekday, last_weekday, offset;
3891 guint16 year;
3892 guint8 month;
3893
3894 /* If BYDAY has not been specified, or the array is empty, just
3895 * return the array. */
3896 if (!recur_data->recur->byday || occs->len == 0)
3897 return occs;
3898
3899 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3900
3901 len = occs->len;
3902 for (i = 0; i < len; i++) {
3903 occ = &g_array_index (occs, CalObjTime, i);
3904
3905 elem = recur_data->recur->byday;
3906 while (elem) {
3907 weekday = GPOINTER_TO_INT (elem->data);
3908 elem = elem->next;
3909 week_num = GPOINTER_TO_INT (elem->data);
3910 elem = elem->next;
3911
3912 year = occ->year;
3913 month = occ->month;
3914 if (week_num == 0) {
3915 /* Expand to every Mon/Tue/etc. in the month.*/
3916 occ->day = 1;
3917 first_weekday = cal_obj_time_weekday (occ);
3918 offset = (weekday + 7 - first_weekday) % 7;
3919 cal_obj_time_add_days (occ, offset);
3920
3921 while (occ->year == year
3922 && occ->month == month) {
3923 g_array_append_vals (new_occs, occ, 1);
3924 cal_obj_time_add_days (occ, 7);
3925 }
3926
3927 } else if (week_num > 0) {
3928 /* Add the nth Mon/Tue/etc. in the month. */
3929 occ->day = 1;
3930 first_weekday = cal_obj_time_weekday (occ);
3931 offset = (weekday + 7 - first_weekday) % 7;
3932 offset += (week_num - 1) * 7;
3933 cal_obj_time_add_days (occ, offset);
3934 if (occ->year == year && occ->month == month)
3935 g_array_append_vals (new_occs, occ, 1);
3936
3937 } else {
3938 /* Add the -nth Mon/Tue/etc. in the month. */
3939 occ->day = time_days_in_month (
3940 occ->year,
3941 occ->month);
3942 last_weekday = cal_obj_time_weekday (occ);
3943
3944 /* This calculates the number of days to step
3945 * backwards from the last day of the month
3946 * to the weekday we want. */
3947 offset = (last_weekday + 7 - weekday) % 7;
3948
3949 /* This adds on the weeks. */
3950 offset += (-week_num - 1) * 7;
3951
3952 cal_obj_time_add_days (occ, -offset);
3953 if (occ->year == year && occ->month == month)
3954 g_array_append_vals (new_occs, occ, 1);
3955 }
3956
3957 /* Reset the year & month, as we may have gone past
3958 * the end. */
3959 occ->year = year;
3960 occ->month = month;
3961 }
3962 }
3963
3964 g_array_free (occs, TRUE);
3965
3966 return new_occs;
3967 }
3968
3969 /* Note: occs must not contain invalid dates, e.g. 31st September. */
3970 static GArray *
cal_obj_byday_expand_weekly(RecurData * recur_data,GArray * occs)3971 cal_obj_byday_expand_weekly (RecurData *recur_data,
3972 GArray *occs)
3973 {
3974 GArray *new_occs;
3975 CalObjTime *occ;
3976 GList *elem;
3977 gint len, i, weekday;
3978 gint weekday_offset, new_weekday_offset;
3979
3980 /* If BYDAY has not been specified, or the array is empty, just
3981 * return the array. */
3982 if (!recur_data->recur->byday || occs->len == 0)
3983 return occs;
3984
3985 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
3986
3987 len = occs->len;
3988 for (i = 0; i < len; i++) {
3989 occ = &g_array_index (occs, CalObjTime, i);
3990
3991 elem = recur_data->recur->byday;
3992 while (elem) {
3993 weekday = GPOINTER_TO_INT (elem->data);
3994 elem = elem->next;
3995
3996 /* FIXME: Currently we just ignore this, but maybe we
3997 * should skip all elements where week_num != 0.
3998 * The spec isn't clear about this. */
3999 /*week_num = GPOINTER_TO_INT (elem->data);*/
4000 elem = elem->next;
4001
4002 weekday_offset = cal_obj_time_weekday_offset (occ, recur_data->recur);
4003 new_weekday_offset = (weekday + 7 - recur_data->recur->week_start_day) % 7;
4004 cal_obj_time_add_days (occ, new_weekday_offset - weekday_offset);
4005 g_array_append_vals (new_occs, occ, 1);
4006 }
4007 }
4008
4009 g_array_free (occs, TRUE);
4010
4011 return new_occs;
4012 }
4013
4014 /* Note: occs must not contain invalid dates, e.g. 31st September. */
4015 static GArray *
cal_obj_byday_filter(RecurData * recur_data,GArray * occs)4016 cal_obj_byday_filter (RecurData *recur_data,
4017 GArray *occs)
4018 {
4019 GArray *new_occs;
4020 CalObjTime *occ;
4021 gint len, i, weekday;
4022
4023 /* If BYDAY has not been specified, or the array is empty, just
4024 * return the array. */
4025 if (!recur_data->recur->byday || occs->len == 0)
4026 return occs;
4027
4028 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4029
4030 len = occs->len;
4031 for (i = 0; i < len; i++) {
4032 occ = &g_array_index (occs, CalObjTime, i);
4033 weekday = cal_obj_time_weekday (occ);
4034
4035 /* See if the weekday on its own is set. */
4036 if (recur_data->weekdays[weekday])
4037 g_array_append_vals (new_occs, occ, 1);
4038 }
4039
4040 g_array_free (occs, TRUE);
4041
4042 return new_occs;
4043 }
4044
4045 /* If the BYHOUR rule is specified it expands each occurrence in occs, by
4046 * using each of the hours in the byhour list. */
4047 static GArray *
cal_obj_byhour_expand(RecurData * recur_data,GArray * occs)4048 cal_obj_byhour_expand (RecurData *recur_data,
4049 GArray *occs)
4050 {
4051 GArray *new_occs;
4052 CalObjTime *occ;
4053 GList *elem;
4054 gint len, i;
4055
4056 /* If BYHOUR has not been specified, or the array is empty, just
4057 * return the array. */
4058 if (!recur_data->recur->byhour || occs->len == 0)
4059 return occs;
4060
4061 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4062
4063 len = occs->len;
4064 for (i = 0; i < len; i++) {
4065 occ = &g_array_index (occs, CalObjTime, i);
4066
4067 elem = recur_data->recur->byhour;
4068 while (elem) {
4069 occ->hour = GPOINTER_TO_INT (elem->data);
4070 g_array_append_vals (new_occs, occ, 1);
4071 elem = elem->next;
4072 }
4073 }
4074
4075 g_array_free (occs, TRUE);
4076
4077 return new_occs;
4078 }
4079
4080 /* If the BYHOUR rule is specified it filters out all occurrences in occs
4081 * which do not match one of the hours in the byhour list. */
4082 static GArray *
cal_obj_byhour_filter(RecurData * recur_data,GArray * occs)4083 cal_obj_byhour_filter (RecurData *recur_data,
4084 GArray *occs)
4085 {
4086 GArray *new_occs;
4087 CalObjTime *occ;
4088 gint len, i;
4089
4090 /* If BYHOUR has not been specified, or the array is empty, just
4091 * return the array. */
4092 if (!recur_data->recur->byhour || occs->len == 0)
4093 return occs;
4094
4095 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4096
4097 len = occs->len;
4098 for (i = 0; i < len; i++) {
4099 occ = &g_array_index (occs, CalObjTime, i);
4100 if (recur_data->hours[occ->hour])
4101 g_array_append_vals (new_occs, occ, 1);
4102 }
4103
4104 g_array_free (occs, TRUE);
4105
4106 return new_occs;
4107 }
4108
4109 /* If the BYMINUTE rule is specified it expands each occurrence in occs, by
4110 * using each of the minutes in the byminute list. */
4111 static GArray *
cal_obj_byminute_expand(RecurData * recur_data,GArray * occs)4112 cal_obj_byminute_expand (RecurData *recur_data,
4113 GArray *occs)
4114 {
4115 GArray *new_occs;
4116 CalObjTime *occ;
4117 GList *elem;
4118 gint len, i;
4119
4120 /* If BYMINUTE has not been specified, or the array is empty, just
4121 * return the array. */
4122 if (!recur_data->recur->byminute || occs->len == 0)
4123 return occs;
4124
4125 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4126
4127 len = occs->len;
4128 for (i = 0; i < len; i++) {
4129 occ = &g_array_index (occs, CalObjTime, i);
4130
4131 elem = recur_data->recur->byminute;
4132 while (elem) {
4133 occ->minute = GPOINTER_TO_INT (elem->data);
4134 g_array_append_vals (new_occs, occ, 1);
4135 elem = elem->next;
4136 }
4137 }
4138
4139 g_array_free (occs, TRUE);
4140
4141 return new_occs;
4142 }
4143
4144 /* If the BYMINUTE rule is specified it filters out all occurrences in occs
4145 * which do not match one of the minutes in the byminute list. */
4146 static GArray *
cal_obj_byminute_filter(RecurData * recur_data,GArray * occs)4147 cal_obj_byminute_filter (RecurData *recur_data,
4148 GArray *occs)
4149 {
4150 GArray *new_occs;
4151 CalObjTime *occ;
4152 gint len, i;
4153
4154 /* If BYMINUTE has not been specified, or the array is empty, just
4155 * return the array. */
4156 if (!recur_data->recur->byminute || occs->len == 0)
4157 return occs;
4158
4159 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4160
4161 len = occs->len;
4162 for (i = 0; i < len; i++) {
4163 occ = &g_array_index (occs, CalObjTime, i);
4164 if (recur_data->minutes[occ->minute])
4165 g_array_append_vals (new_occs, occ, 1);
4166 }
4167
4168 g_array_free (occs, TRUE);
4169
4170 return new_occs;
4171 }
4172
4173 /* If the BYSECOND rule is specified it expands each occurrence in occs, by
4174 * using each of the seconds in the bysecond list. */
4175 static GArray *
cal_obj_bysecond_expand(RecurData * recur_data,GArray * occs)4176 cal_obj_bysecond_expand (RecurData *recur_data,
4177 GArray *occs)
4178 {
4179 GArray *new_occs;
4180 CalObjTime *occ;
4181 GList *elem;
4182 gint len, i;
4183
4184 /* If BYSECOND has not been specified, or the array is empty, just
4185 * return the array. */
4186 if (!recur_data->recur->bysecond || occs->len == 0)
4187 return occs;
4188
4189 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4190
4191 len = occs->len;
4192 for (i = 0; i < len; i++) {
4193 occ = &g_array_index (occs, CalObjTime, i);
4194
4195 elem = recur_data->recur->bysecond;
4196 while (elem) {
4197 occ->second = GPOINTER_TO_INT (elem->data);
4198 g_array_append_vals (new_occs, occ, 1);
4199 elem = elem->next;
4200 }
4201 }
4202
4203 g_array_free (occs, TRUE);
4204
4205 return new_occs;
4206 }
4207
4208 /* If the BYSECOND rule is specified it filters out all occurrences in occs
4209 * which do not match one of the seconds in the bysecond list. */
4210 static GArray *
cal_obj_bysecond_filter(RecurData * recur_data,GArray * occs)4211 cal_obj_bysecond_filter (RecurData *recur_data,
4212 GArray *occs)
4213 {
4214 GArray *new_occs;
4215 CalObjTime *occ;
4216 gint len, i;
4217
4218 /* If BYSECOND has not been specified, or the array is empty, just
4219 * return the array. */
4220 if (!recur_data->recur->bysecond || occs->len == 0)
4221 return occs;
4222
4223 new_occs = g_array_new (FALSE, FALSE, sizeof (CalObjTime));
4224
4225 len = occs->len;
4226 for (i = 0; i < len; i++) {
4227 occ = &g_array_index (occs, CalObjTime, i);
4228 if (recur_data->seconds[occ->second])
4229 g_array_append_vals (new_occs, occ, 1);
4230 }
4231
4232 g_array_free (occs, TRUE);
4233
4234 return new_occs;
4235 }
4236
4237 /* Adds a positive or negative number of months to the given CalObjTime,
4238 * updating the year appropriately so we end up with a valid month.
4239 * Note that the day may be invalid, e.g. 30th Feb. */
4240 static void
cal_obj_time_add_months(CalObjTime * cotime,gint months)4241 cal_obj_time_add_months (CalObjTime *cotime,
4242 gint months)
4243 {
4244 guint month, years;
4245
4246 /* We use a guint to avoid overflow on the guint8. */
4247 month = cotime->month + months;
4248 cotime->month = month % 12;
4249 if (month > 0) {
4250 cotime->year += month / 12;
4251 } else {
4252 years = month / 12;
4253 if (cotime->month != 0) {
4254 cotime->month += 12;
4255 years -= 1;
4256 }
4257 cotime->year += years;
4258 }
4259 }
4260
4261 /* Adds a positive or negative number of days to the given CalObjTime,
4262 * updating the month and year appropriately so we end up with a valid day. */
4263 static void
cal_obj_time_add_days(CalObjTime * cotime,gint days)4264 cal_obj_time_add_days (CalObjTime *cotime,
4265 gint days)
4266 {
4267 gint day, days_in_month;
4268
4269 /* We use a guint to avoid overflow on the guint8. */
4270 day = cotime->day;
4271 day += days;
4272
4273 if (days >= 0) {
4274 for (;;) {
4275 days_in_month = time_days_in_month (
4276 cotime->year,
4277 cotime->month);
4278 if (day <= days_in_month)
4279 break;
4280
4281 cotime->month++;
4282 if (cotime->month >= 12) {
4283 cotime->year++;
4284 cotime->month = 0;
4285 }
4286
4287 day -= days_in_month;
4288 }
4289
4290 cotime->day = (guint8) day;
4291 } else {
4292 while (day <= 0) {
4293 if (cotime->month == 0) {
4294 cotime->year--;
4295 cotime->month = 11;
4296 } else {
4297 cotime->month--;
4298 }
4299
4300 days_in_month = time_days_in_month (
4301 cotime->year,
4302 cotime->month);
4303 day += days_in_month;
4304 }
4305
4306 cotime->day = (guint8) day;
4307 }
4308 }
4309
4310 /* Adds a positive or negative number of hours to the given CalObjTime,
4311 * updating the day, month & year appropriately so we end up with a valid
4312 * time. */
4313 static void
cal_obj_time_add_hours(CalObjTime * cotime,gint hours)4314 cal_obj_time_add_hours (CalObjTime *cotime,
4315 gint hours)
4316 {
4317 gint hour, days;
4318
4319 /* We use a gint to avoid overflow on the guint8. */
4320 hour = cotime->hour + hours;
4321 cotime->hour = hour % 24;
4322 if (hour >= 0) {
4323 if (hour >= 24)
4324 cal_obj_time_add_days (cotime, hour / 24);
4325 } else {
4326 days = hour / 24;
4327 if (cotime->hour != 0) {
4328 cotime->hour += 24;
4329 days -= 1;
4330 }
4331 cal_obj_time_add_days (cotime, days);
4332 }
4333 }
4334
4335 /* Adds a positive or negative number of minutes to the given CalObjTime,
4336 * updating the rest of the CalObjTime appropriately. */
4337 static void
cal_obj_time_add_minutes(CalObjTime * cotime,gint minutes)4338 cal_obj_time_add_minutes (CalObjTime *cotime,
4339 gint minutes)
4340 {
4341 gint minute, hours;
4342
4343 /* We use a gint to avoid overflow on the guint8. */
4344 minute = cotime->minute + minutes;
4345 cotime->minute = minute % 60;
4346 if (minute >= 0) {
4347 if (minute >= 60)
4348 cal_obj_time_add_hours (cotime, minute / 60);
4349 } else {
4350 hours = minute / 60;
4351 if (cotime->minute != 0) {
4352 cotime->minute += 60;
4353 hours -= 1;
4354 }
4355 cal_obj_time_add_hours (cotime, hours);
4356 }
4357 }
4358
4359 /* Adds a positive or negative number of seconds to the given CalObjTime,
4360 * updating the rest of the CalObjTime appropriately. */
4361 static void
cal_obj_time_add_seconds(CalObjTime * cotime,gint seconds)4362 cal_obj_time_add_seconds (CalObjTime *cotime,
4363 gint seconds)
4364 {
4365 gint second, minutes;
4366
4367 /* We use a gint to avoid overflow on the guint8. */
4368 second = cotime->second + seconds;
4369 cotime->second = second % 60;
4370 if (second >= 0) {
4371 if (second >= 60)
4372 cal_obj_time_add_minutes (cotime, second / 60);
4373 } else {
4374 minutes = second / 60;
4375 if (cotime->second != 0) {
4376 cotime->second += 60;
4377 minutes -= 1;
4378 }
4379 cal_obj_time_add_minutes (cotime, minutes);
4380 }
4381 }
4382
4383 /* Compares 2 CalObjTimes. Returns -1 if the cotime1 is before cotime2, 0 if
4384 * they are the same, or 1 if cotime1 is after cotime2. The comparison type
4385 * specifies which parts of the times we are interested in, e.g. if CALOBJ_DAY
4386 * is used we only want to know if the days are different. */
4387 static gint
cal_obj_time_compare(CalObjTime * cotime1,CalObjTime * cotime2,CalObjTimeComparison type)4388 cal_obj_time_compare (CalObjTime *cotime1,
4389 CalObjTime *cotime2,
4390 CalObjTimeComparison type)
4391 {
4392 if (cotime1->year < cotime2->year)
4393 return -1;
4394 if (cotime1->year > cotime2->year)
4395 return 1;
4396
4397 if (type == CALOBJ_YEAR)
4398 return 0;
4399
4400 if (cotime1->month < cotime2->month)
4401 return -1;
4402 if (cotime1->month > cotime2->month)
4403 return 1;
4404
4405 if (type == CALOBJ_MONTH)
4406 return 0;
4407
4408 if (cotime1->day < cotime2->day)
4409 return -1;
4410 if (cotime1->day > cotime2->day)
4411 return 1;
4412
4413 if (type == CALOBJ_DAY)
4414 return 0;
4415
4416 if (cotime1->hour < cotime2->hour)
4417 return -1;
4418 if (cotime1->hour > cotime2->hour)
4419 return 1;
4420
4421 if (type == CALOBJ_HOUR)
4422 return 0;
4423
4424 if (cotime1->minute < cotime2->minute)
4425 return -1;
4426 if (cotime1->minute > cotime2->minute)
4427 return 1;
4428
4429 if (type == CALOBJ_MINUTE)
4430 return 0;
4431
4432 if (cotime1->second < cotime2->second)
4433 return -1;
4434 if (cotime1->second > cotime2->second)
4435 return 1;
4436
4437 return 0;
4438 }
4439
4440 /* This is the same as the above function, but without the comparison type.
4441 * It is used for qsort (). */
4442 static gint
cal_obj_time_compare_func(gconstpointer arg1,gconstpointer arg2)4443 cal_obj_time_compare_func (gconstpointer arg1,
4444 gconstpointer arg2)
4445 {
4446 CalObjTime *cotime1, *cotime2;
4447 gint retval;
4448
4449 cotime1 = (CalObjTime *) arg1;
4450 cotime2 = (CalObjTime *) arg2;
4451
4452 if (!cotime1 || !cotime2) {
4453 if (cotime1 == cotime2)
4454 return 0;
4455
4456 return cotime2 ? -1 : 1;
4457 }
4458
4459 if (cotime1->year < cotime2->year)
4460 retval = -1;
4461 else if (cotime1->year > cotime2->year)
4462 retval = 1;
4463
4464 else if (cotime1->month < cotime2->month)
4465 retval = -1;
4466 else if (cotime1->month > cotime2->month)
4467 retval = 1;
4468
4469 else if (cotime1->day < cotime2->day)
4470 retval = -1;
4471 else if (cotime1->day > cotime2->day)
4472 retval = 1;
4473
4474 else if (cotime1->hour < cotime2->hour)
4475 retval = -1;
4476 else if (cotime1->hour > cotime2->hour)
4477 retval = 1;
4478
4479 else if (cotime1->minute < cotime2->minute)
4480 retval = -1;
4481 else if (cotime1->minute > cotime2->minute)
4482 retval = 1;
4483
4484 else if (cotime1->second < cotime2->second)
4485 retval = -1;
4486 else if (cotime1->second > cotime2->second)
4487 retval = 1;
4488
4489 else
4490 retval = 0;
4491
4492 #if 0
4493 g_print ("%s - ", cal_obj_time_to_string (cotime1));
4494 g_print ("%s : %i\n", cal_obj_time_to_string (cotime2), retval);
4495 #endif
4496
4497 return retval;
4498 }
4499
4500 static gint
cal_obj_date_only_compare_func(gconstpointer arg1,gconstpointer arg2)4501 cal_obj_date_only_compare_func (gconstpointer arg1,
4502 gconstpointer arg2)
4503 {
4504 CalObjTime *cotime1, *cotime2;
4505
4506 cotime1 = (CalObjTime *) arg1;
4507 cotime2 = (CalObjTime *) arg2;
4508
4509 if (cotime1->year < cotime2->year)
4510 return -1;
4511 if (cotime1->year > cotime2->year)
4512 return 1;
4513
4514 if (cotime1->month < cotime2->month)
4515 return -1;
4516 if (cotime1->month > cotime2->month)
4517 return 1;
4518
4519 if (cotime1->day < cotime2->day)
4520 return -1;
4521 if (cotime1->day > cotime2->day)
4522 return 1;
4523
4524 return 0;
4525 }
4526
4527 /* Returns the weekday of the given CalObjTime, from 0 (Mon) - 6 (Sun). */
4528 static gint
cal_obj_time_weekday(CalObjTime * cotime)4529 cal_obj_time_weekday (CalObjTime *cotime)
4530 {
4531 GDate date;
4532 gint weekday;
4533
4534 g_date_clear (&date, 1);
4535 g_date_set_dmy (&date, cotime->day, cotime->month + 1, cotime->year);
4536
4537 /* This results in a value of 0 (Monday) - 6 (Sunday). */
4538 weekday = g_date_get_weekday (&date) - 1;
4539
4540 return weekday;
4541 }
4542
4543 /* Returns the weekday of the given CalObjTime, from 0 - 6. The week start
4544 * day is Monday by default, but can be set in the recurrence rule. */
4545 static gint
cal_obj_time_weekday_offset(CalObjTime * cotime,ECalRecurrence * recur)4546 cal_obj_time_weekday_offset (CalObjTime *cotime,
4547 ECalRecurrence *recur)
4548 {
4549 GDate date;
4550 gint weekday, offset;
4551
4552 g_date_clear (&date, 1);
4553 g_date_set_dmy (&date, cotime->day, cotime->month + 1, cotime->year);
4554
4555 /* This results in a value of 0 (Monday) - 6 (Sunday). */
4556 weekday = g_date_get_weekday (&date) - 1;
4557
4558 /* This calculates the offset of our day from the start of the week.
4559 * We just add on a week (to avoid any possible negative values) and
4560 * then subtract the specified week start day, then convert it into a
4561 * value from 0-6. */
4562 offset = (weekday + 7 - recur->week_start_day) % 7;
4563
4564 return offset;
4565 }
4566
4567 /* Returns the day of the year of the given CalObjTime, from 1 - 366. */
4568 static gint
cal_obj_time_day_of_year(CalObjTime * cotime)4569 cal_obj_time_day_of_year (CalObjTime *cotime)
4570 {
4571 GDate date;
4572
4573 g_date_clear (&date, 1);
4574 g_date_set_dmy (&date, cotime->day, cotime->month + 1, cotime->year);
4575
4576 return g_date_get_day_of_year (&date);
4577 }
4578
4579 /* Finds the first week in the given CalObjTime's year, using the same weekday
4580 * as the event start day (i.e. from the RecurData).
4581 * The first week of the year is the first week starting from the specified
4582 * week start day that has 4 days in the new year. It may be in the previous
4583 * year. */
4584 static void
cal_obj_time_find_first_week(CalObjTime * cotime,RecurData * recur_data)4585 cal_obj_time_find_first_week (CalObjTime *cotime,
4586 RecurData *recur_data)
4587 {
4588 GDate date;
4589 gint weekday, week_start_day, first_full_week_start_offset, offset;
4590
4591 /* Find out the weekday of the 1st of the year, 0 (Mon) - 6 (Sun). */
4592 g_date_clear (&date, 1);
4593 g_date_set_dmy (&date, 1, 1, cotime->year);
4594 weekday = g_date_get_weekday (&date) - 1;
4595
4596 /* Calculate the first day of the year that starts a new week, i.e. the
4597 * first week_start_day after weekday, using 0 = 1st Jan.
4598 * e.g. if the 1st Jan is a Tuesday (1) and week_start_day is a
4599 * Monday (0), the result will be (0 + 7 - 1) % 7 = 6 (7th Jan). */
4600 week_start_day = recur_data->recur->week_start_day;
4601 first_full_week_start_offset = (week_start_day + 7 - weekday) % 7;
4602
4603 /* Now see if we have to move backwards 1 week, i.e. if the week
4604 * starts on or after Jan 5th (since the previous week has 4 days in
4605 * this year and so will be the first week of the year). */
4606 if (first_full_week_start_offset >= 4)
4607 first_full_week_start_offset -= 7;
4608
4609 /* Now add the days to get to the event's weekday. */
4610 offset = first_full_week_start_offset + recur_data->weekday_offset;
4611
4612 /* Now move the cotime to the appropriate day. */
4613 cotime->month = 0;
4614 cotime->day = 1;
4615 cal_obj_time_add_days (cotime, offset);
4616 }
4617
4618 static void
cal_object_time_from_time(CalObjTime * cotime,time_t t,ICalTimezone * zone)4619 cal_object_time_from_time (CalObjTime *cotime,
4620 time_t t,
4621 ICalTimezone *zone)
4622 {
4623 ICalTime *tt;
4624
4625 tt = i_cal_time_new_from_timet_with_zone (t, FALSE, zone);
4626
4627 cotime->year = i_cal_time_get_year (tt);
4628 cotime->month = i_cal_time_get_month (tt) - 1;
4629 cotime->day = i_cal_time_get_day (tt);
4630 cotime->hour = i_cal_time_get_hour (tt);
4631 cotime->minute = i_cal_time_get_minute (tt);
4632 cotime->second = i_cal_time_get_second (tt);
4633 cotime->flags = FALSE;
4634
4635 g_clear_object (&tt);
4636 }
4637
4638 /* Debugging function to convert a CalObjTime to a string. It uses a static
4639 * buffer so beware. */
4640 #ifdef CAL_OBJ_DEBUG
4641 static const gchar *
cal_obj_time_to_string(CalObjTime * cotime)4642 cal_obj_time_to_string (CalObjTime *cotime)
4643 {
4644 static gchar buffer[50];
4645 gchar *weekdays[] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
4646 " " };
4647 gint weekday;
4648
4649 weekday = cal_obj_time_weekday (cotime);
4650
4651 g_snprintf (
4652 buffer, sizeof (buffer),
4653 "%s %02i/%02i/%04i %02i:%02i:%02i",
4654 weekdays[weekday],
4655 cotime->day, cotime->month + 1, cotime->year,
4656 cotime->hour, cotime->minute, cotime->second);
4657
4658 return buffer;
4659 }
4660 #endif
4661
4662 /**
4663 * e_cal_recur_ensure_end_dates:
4664 * @comp: an #ECalComponent
4665 * @refresh: %TRUE to recalculate all end dates
4666 * @tz_cb: (scope call): function to call to resolve timezones
4667 * @tz_cb_data: (closure tz_cb_data): user data to pass to @tz_cb
4668 * @cancellable: optional #GCancellable object, or %NULL
4669 * @error: return location for a #GError, or %NULL
4670 *
4671 * This recalculates the end dates for recurrence & exception rules which use
4672 * the COUNT property. If @refresh is %TRUE it will recalculate all enddates
4673 * for rules which use COUNT. If @refresh is %FALSE, it will only calculate
4674 * the enddate if it hasn't already been set. It returns %TRUE if the component
4675 * was changed, i.e. if the component should be saved at some point.
4676 * We store the enddate in the %E_CAL_EVOLUTION_ENDDATE_PARAMETER parameter of the RRULE
4677 * or EXRULE.
4678 *
4679 * Returns: %TRUE if the component was changed, %FALSE otherwise
4680 *
4681 * Since: 2.32
4682 **/
4683 gboolean
e_cal_recur_ensure_end_dates(ECalComponent * comp,gboolean refresh,ECalRecurResolveTimezoneCb tz_cb,gpointer tz_cb_data,GCancellable * cancellable,GError ** error)4684 e_cal_recur_ensure_end_dates (ECalComponent *comp,
4685 gboolean refresh,
4686 ECalRecurResolveTimezoneCb tz_cb,
4687 gpointer tz_cb_data,
4688 GCancellable *cancellable,
4689 GError **error)
4690 {
4691 GSList *rrules, *exrules, *elem;
4692 gboolean changed = FALSE;
4693
4694 /* Do the RRULEs. */
4695 rrules = e_cal_component_get_rrule_properties (comp);
4696 for (elem = rrules; elem && !g_cancellable_is_cancelled (cancellable); elem = elem->next) {
4697 changed |= e_cal_recur_ensure_rule_end_date (comp, elem->data,
4698 FALSE, refresh, tz_cb, tz_cb_data, cancellable, error);
4699 }
4700
4701 /* Do the EXRULEs. */
4702 exrules = e_cal_component_get_exrule_properties (comp);
4703 for (elem = exrules; elem && !g_cancellable_is_cancelled (cancellable); elem = elem->next) {
4704 changed |= e_cal_recur_ensure_rule_end_date (comp, elem->data,
4705 TRUE, refresh, tz_cb, tz_cb_data, cancellable, error);
4706 }
4707
4708 g_slist_free_full (rrules, g_object_unref);
4709 g_slist_free_full (exrules, g_object_unref);
4710
4711 return changed;
4712 }
4713
4714 typedef struct _ECalRecurEnsureEndDateData ECalRecurEnsureEndDateData;
4715 struct _ECalRecurEnsureEndDateData {
4716 gint count;
4717 gint instances;
4718 time_t end_date;
4719 };
4720
4721 static gboolean
e_cal_recur_ensure_rule_end_date(ECalComponent * comp,ICalProperty * prop,gboolean exception,gboolean refresh,ECalRecurResolveTimezoneCb tz_cb,gpointer tz_cb_data,GCancellable * cancellable,GError ** error)4722 e_cal_recur_ensure_rule_end_date (ECalComponent *comp,
4723 ICalProperty *prop,
4724 gboolean exception,
4725 gboolean refresh,
4726 ECalRecurResolveTimezoneCb tz_cb,
4727 gpointer tz_cb_data,
4728 GCancellable *cancellable,
4729 GError **error)
4730 {
4731 ICalRecurrence *rule;
4732 ECalRecurEnsureEndDateData cb_data;
4733
4734 if (!prop)
4735 return FALSE;
4736
4737 if (exception)
4738 rule = i_cal_property_get_exrule (prop);
4739 else
4740 rule = i_cal_property_get_rrule (prop);
4741
4742 /* If the rule doesn't use COUNT just return. */
4743 if (!rule || !i_cal_recurrence_get_count (rule)) {
4744 g_clear_object (&rule);
4745 return FALSE;
4746 }
4747
4748 /* If refresh is FALSE, we check if the enddate is already set, and
4749 * if it is we just return. */
4750 if (!refresh) {
4751 if (e_cal_recur_get_rule_end_date (prop, NULL) != -1) {
4752 g_clear_object (&rule);
4753 return FALSE;
4754 }
4755 } else {
4756 /* Remove the property parameter, thus it'll not influence the regeneration */
4757 i_cal_property_remove_parameter_by_name (prop, E_CAL_EVOLUTION_ENDDATE_PARAMETER);
4758 }
4759
4760 /* Calculate the end date. Note that we initialize end_date to 0, so
4761 * if the RULE doesn't generate COUNT instances we save a time_t of 0.
4762 * Also note that we use the UTC timezone as the default timezone.
4763 * In get_end_date () if the DTSTART is a DATE or floating time, we will
4764 * convert the ENDDATE to the current timezone. */
4765 cb_data.count = i_cal_recurrence_get_count (rule);
4766 cb_data.instances = 0;
4767 cb_data.end_date = 0;
4768 e_cal_recur_generate_instances_of_rule (
4769 comp, prop, -1, -1,
4770 e_cal_recur_ensure_rule_end_date_cb,
4771 &cb_data, tz_cb, tz_cb_data,
4772 i_cal_timezone_get_utc_timezone ());
4773
4774 /* Store the end date in the E_CAL_EVOLUTION_ENDDATE_PARAMETER parameter of the rule. */
4775 e_cal_recur_set_rule_end_date (prop, cb_data.end_date);
4776
4777 g_clear_object (&rule);
4778
4779 return TRUE;
4780 }
4781
4782 static gboolean
e_cal_recur_ensure_rule_end_date_cb(ICalComponent * comp,ICalTime * instance_start,ICalTime * instance_end,gpointer user_data,GCancellable * cancellable,GError ** error)4783 e_cal_recur_ensure_rule_end_date_cb (ICalComponent *comp,
4784 ICalTime *instance_start,
4785 ICalTime *instance_end,
4786 gpointer user_data,
4787 GCancellable *cancellable,
4788 GError **error)
4789 {
4790 ECalRecurEnsureEndDateData *cb_data = user_data;
4791
4792 cb_data->instances++;
4793
4794 if (cb_data->instances == cb_data->count) {
4795 cb_data->end_date = i_cal_time_as_timet (instance_start);
4796 return FALSE;
4797 }
4798
4799 return TRUE;
4800 }
4801
4802 /* If default_timezone is set, the saved ENDDATE parameter is assumed to be
4803 * in that timezone. This is used when the DTSTART is a DATE or floating
4804 * value, since the RRULE end date will change depending on the timezone that
4805 * it is evaluated in. */
4806 static time_t
e_cal_recur_get_rule_end_date(ICalProperty * prop,ICalTimezone * default_timezone)4807 e_cal_recur_get_rule_end_date (ICalProperty *prop,
4808 ICalTimezone *default_timezone)
4809 {
4810 ICalParameter *param;
4811 const gchar *xname, *xvalue;
4812
4813 for (param = i_cal_property_get_first_parameter (prop, I_CAL_X_PARAMETER);
4814 param;
4815 g_object_unref (param), param = i_cal_property_get_next_parameter (prop, I_CAL_X_PARAMETER)) {
4816 xname = i_cal_parameter_get_xname (param);
4817 if (xname && !strcmp (xname, E_CAL_EVOLUTION_ENDDATE_PARAMETER)) {
4818 ICalValue *value;
4819
4820 xvalue = i_cal_parameter_get_x (param);
4821 value = i_cal_value_new_from_string (I_CAL_DATETIME_VALUE, xvalue);
4822 if (value) {
4823 ICalTime *tt;
4824 ICalTimezone *zone;
4825 time_t res;
4826
4827 tt = i_cal_value_get_datetime (value);
4828 g_object_unref (value);
4829
4830 zone = default_timezone;
4831 if (!zone)
4832 zone = i_cal_timezone_get_utc_timezone ();
4833
4834 res = i_cal_time_as_timet_with_zone (tt, zone);
4835
4836 g_object_unref (param);
4837 g_clear_object (&tt);
4838
4839 return res;
4840 }
4841 }
4842 }
4843
4844 return -1;
4845 }
4846
4847 static void
e_cal_recur_set_rule_end_date(ICalProperty * prop,time_t end_date)4848 e_cal_recur_set_rule_end_date (ICalProperty *prop,
4849 time_t end_date)
4850 {
4851 ICalParameter *param;
4852 ICalValue *value;
4853 ICalTimezone *utc_zone;
4854 ICalTime *icaltime;
4855 const gchar *xname;
4856 gchar *end_date_string;
4857
4858 /* We save the value as a UTC DATE-TIME. */
4859 utc_zone = i_cal_timezone_get_utc_timezone ();
4860 icaltime = i_cal_time_new_from_timet_with_zone (end_date, FALSE, utc_zone);
4861 value = i_cal_value_new_datetime (icaltime);
4862 end_date_string = i_cal_value_as_ical_string (value);
4863 g_object_unref (value);
4864 g_object_unref (icaltime);
4865
4866 /* If we already have an X-EVOLUTION-ENDDATE parameter, set the value
4867 * to the new date-time. */
4868 for (param = i_cal_property_get_first_parameter (prop, I_CAL_X_PARAMETER);
4869 param;
4870 g_object_unref (param), param = i_cal_property_get_next_parameter (prop, I_CAL_X_PARAMETER)) {
4871 xname = i_cal_parameter_get_xname (param);
4872 if (xname && !strcmp (xname, E_CAL_EVOLUTION_ENDDATE_PARAMETER)) {
4873 i_cal_parameter_set_x (param, end_date_string);
4874 g_free (end_date_string);
4875 g_object_unref (param);
4876 return;
4877 }
4878 }
4879
4880 /* Create a new X-EVOLUTION-ENDDATE and add it to the property. */
4881 param = i_cal_parameter_new_x (end_date_string);
4882 i_cal_parameter_set_xname (param, E_CAL_EVOLUTION_ENDDATE_PARAMETER);
4883 i_cal_property_take_parameter (prop, param);
4884
4885 g_free (end_date_string);
4886 }
4887
4888 static const gchar *e_cal_recur_nth[31] = {
4889 N_("1st"),
4890 N_("2nd"),
4891 N_("3rd"),
4892 N_("4th"),
4893 N_("5th"),
4894 N_("6th"),
4895 N_("7th"),
4896 N_("8th"),
4897 N_("9th"),
4898 N_("10th"),
4899 N_("11th"),
4900 N_("12th"),
4901 N_("13th"),
4902 N_("14th"),
4903 N_("15th"),
4904 N_("16th"),
4905 N_("17th"),
4906 N_("18th"),
4907 N_("19th"),
4908 N_("20th"),
4909 N_("21st"),
4910 N_("22nd"),
4911 N_("23rd"),
4912 N_("24th"),
4913 N_("25th"),
4914 N_("26th"),
4915 N_("27th"),
4916 N_("28th"),
4917 N_("29th"),
4918 N_("30th"),
4919 N_("31st")
4920 };
4921
4922 /**
4923 * e_cal_recur_get_localized_nth:
4924 * @nth: the nth index, counting from zero
4925 *
4926 * Returns: Localized text for the nth position, counting from zero, which means
4927 * for '0' it'll return "1st", for '1' it'll return "2nd" and so on, up to 30,
4928 * when it'll return "31st".
4929 *
4930 * Since: 3.28
4931 **/
4932 const gchar *
e_cal_recur_get_localized_nth(gint nth)4933 e_cal_recur_get_localized_nth (gint nth)
4934 {
4935 g_return_val_if_fail (nth >= 0 && nth < G_N_ELEMENTS (e_cal_recur_nth), NULL);
4936
4937 return _(e_cal_recur_nth[nth]);
4938 }
4939
4940 static gint
cal_comp_util_recurrence_count_by_xxx_and_free(GArray * array)4941 cal_comp_util_recurrence_count_by_xxx_and_free (GArray *array) /* gshort */
4942 {
4943 gint ii;
4944
4945 if (!array)
4946 return 0;
4947
4948 for (ii = 0; ii < array->len; ii++) {
4949 if (g_array_index (array, gshort, ii) == I_CAL_RECURRENCE_ARRAY_MAX)
4950 break;
4951 }
4952
4953 g_array_unref (array);
4954
4955 return ii;
4956 }
4957
4958 /**
4959 * e_cal_recur_describe_recurrence_ex:
4960 * @icalcomp: an #ICalComponent
4961 * @week_start_day: a day when the week starts
4962 * @flags: bit-or of #ECalRecurDescribeRecurrenceFlags
4963 * @datetime_fmt_func: (nullable) (scope call): formatting function for date/time value
4964 *
4965 * Describes some simple types of recurrences in a human-readable and localized way.
4966 * The @flags influence the output format and what to do when the @icalcomp
4967 * contains more complicated recurrence, some which the function cannot describe.
4968 *
4969 * The @week_start_day is used for weekly recurrences, to start the list of selected
4970 * days at that day.
4971 *
4972 * If @datetime_fmt_func is %NULL, the e_time_format_date_and_time() is used
4973 * to format data/time value.
4974 *
4975 * Free the returned string with g_free(), when no longer needed.
4976 *
4977 * Returns: (nullable) (transfer full): a newly allocated string, which
4978 * describes the recurrence of the @icalcomp, or #NULL, when the @icalcomp
4979 * doesn't recur or the recurrence is too complicated to describe, also
4980 * according to given @flags.
4981 *
4982 * Since: 3.38
4983 **/
4984 gchar *
e_cal_recur_describe_recurrence_ex(ICalComponent * icalcomp,GDateWeekday week_start_day,guint32 flags,ECalRecurFormatDateTimeFunc datetime_fmt_func)4985 e_cal_recur_describe_recurrence_ex (ICalComponent *icalcomp,
4986 GDateWeekday week_start_day,
4987 guint32 flags,
4988 ECalRecurFormatDateTimeFunc datetime_fmt_func)
4989 {
4990 gchar *prefix = NULL, *mid = NULL, *suffix = NULL, *result = NULL;
4991 ICalProperty *prop;
4992 ICalRecurrence *rrule = NULL;
4993 ICalTime *until = NULL, *dtstart = NULL;
4994 gint n_by_second, n_by_minute, n_by_hour;
4995 gint n_by_day, n_by_month_day, n_by_year_day;
4996 gint n_by_week_no, n_by_month, n_by_set_pos;
4997 gboolean prefixed, fallback;
4998
4999 g_return_val_if_fail (I_CAL_IS_COMPONENT (icalcomp), NULL);
5000
5001 prefixed = (flags & E_CAL_RECUR_DESCRIBE_RECURRENCE_FLAG_PREFIXED) != 0;
5002 fallback = (flags & E_CAL_RECUR_DESCRIBE_RECURRENCE_FLAG_FALLBACK) != 0;
5003
5004 prop = i_cal_component_get_first_property (icalcomp, I_CAL_RRULE_PROPERTY);
5005 if (!prop)
5006 return NULL;
5007
5008 switch (i_cal_component_isa (icalcomp)) {
5009 case I_CAL_VEVENT_COMPONENT:
5010 case I_CAL_VTODO_COMPONENT:
5011 case I_CAL_VJOURNAL_COMPONENT:
5012 break;
5013 default:
5014 g_object_unref (prop);
5015 return NULL;
5016 }
5017
5018 if (i_cal_component_count_properties (icalcomp, I_CAL_RRULE_PROPERTY) != 1 ||
5019 i_cal_component_count_properties (icalcomp, I_CAL_RDATE_PROPERTY) != 0 ||
5020 i_cal_component_count_properties (icalcomp, I_CAL_EXRULE_PROPERTY) != 0)
5021 goto custom;
5022
5023 rrule = i_cal_property_get_rrule (prop);
5024
5025 switch (i_cal_recurrence_get_freq (rrule)) {
5026 case I_CAL_DAILY_RECURRENCE:
5027 case I_CAL_WEEKLY_RECURRENCE:
5028 case I_CAL_MONTHLY_RECURRENCE:
5029 case I_CAL_YEARLY_RECURRENCE:
5030 break;
5031 default:
5032 goto custom;
5033
5034 }
5035
5036 until = i_cal_recurrence_get_until (rrule);
5037 dtstart = i_cal_component_get_dtstart (icalcomp);
5038
5039 n_by_second = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_second_array (rrule));
5040 n_by_minute = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_minute_array (rrule));
5041 n_by_hour = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_hour_array (rrule));
5042 n_by_day = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_day_array (rrule));
5043 n_by_month_day = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_month_day_array (rrule));
5044 n_by_year_day = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_year_day_array (rrule));
5045 n_by_week_no = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_week_no_array (rrule));
5046 n_by_month = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_month_array (rrule));
5047 n_by_set_pos = cal_comp_util_recurrence_count_by_xxx_and_free (i_cal_recurrence_get_by_set_pos_array (rrule));
5048
5049 if (n_by_second != 0 ||
5050 n_by_minute != 0 ||
5051 n_by_hour != 0)
5052 goto custom;
5053
5054 switch (i_cal_recurrence_get_freq (rrule)) {
5055 case I_CAL_DAILY_RECURRENCE:
5056 if (n_by_day != 0 ||
5057 n_by_month_day != 0 ||
5058 n_by_year_day != 0 ||
5059 n_by_week_no != 0 ||
5060 n_by_month != 0 ||
5061 n_by_set_pos != 0)
5062 goto custom;
5063
5064 if (i_cal_recurrence_get_interval (rrule) > 0) {
5065 if (!i_cal_recurrence_get_count (rrule) && (!until || !i_cal_time_get_year (until))) {
5066 if (prefixed) {
5067 result = g_strdup_printf (
5068 g_dngettext (GETTEXT_PACKAGE,
5069 "every day forever",
5070 "every %d days forever",
5071 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5072 } else {
5073 result = g_strdup_printf (
5074 g_dngettext (GETTEXT_PACKAGE,
5075 "Every day forever",
5076 "Every %d days forever",
5077 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5078 }
5079 } else {
5080 if (prefixed) {
5081 prefix = g_strdup_printf (
5082 g_dngettext (GETTEXT_PACKAGE,
5083 "every day",
5084 "every %d days",
5085 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5086 } else {
5087 prefix = g_strdup_printf (
5088 g_dngettext (GETTEXT_PACKAGE,
5089 "Every day",
5090 "Every %d days",
5091 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5092 }
5093 }
5094 }
5095 break;
5096
5097 case I_CAL_WEEKLY_RECURRENCE: {
5098 gint ii, ndays;
5099 guint8 day_mask;
5100 gint day_shift = week_start_day;
5101
5102 /* Sunday is at bit 0 in the day_mask */
5103 if (day_shift < 0 || day_shift >= G_DATE_SUNDAY)
5104 day_shift = 0;
5105
5106 if (n_by_month_day != 0 ||
5107 n_by_year_day != 0 ||
5108 n_by_week_no != 0 ||
5109 n_by_month != 0 ||
5110 n_by_set_pos != 0)
5111 goto custom;
5112
5113 day_mask = 0;
5114
5115 for (ii = 0; ii < 8 && i_cal_recurrence_get_by_day (rrule, ii) != I_CAL_RECURRENCE_ARRAY_MAX; ii++) {
5116 ICalRecurrenceWeekday weekday;
5117 gint pos;
5118
5119 weekday = i_cal_recurrence_day_day_of_week (i_cal_recurrence_get_by_day (rrule, ii));
5120 pos = i_cal_recurrence_day_position (i_cal_recurrence_get_by_day (rrule, ii));
5121
5122 if (pos != 0)
5123 goto custom;
5124
5125 switch (weekday) {
5126 case I_CAL_SUNDAY_WEEKDAY:
5127 day_mask |= 1 << 0;
5128 break;
5129
5130 case I_CAL_MONDAY_WEEKDAY:
5131 day_mask |= 1 << 1;
5132 break;
5133
5134 case I_CAL_TUESDAY_WEEKDAY:
5135 day_mask |= 1 << 2;
5136 break;
5137
5138 case I_CAL_WEDNESDAY_WEEKDAY:
5139 day_mask |= 1 << 3;
5140 break;
5141
5142 case I_CAL_THURSDAY_WEEKDAY:
5143 day_mask |= 1 << 4;
5144 break;
5145
5146 case I_CAL_FRIDAY_WEEKDAY:
5147 day_mask |= 1 << 5;
5148 break;
5149
5150 case I_CAL_SATURDAY_WEEKDAY:
5151 day_mask |= 1 << 6;
5152 break;
5153
5154 default:
5155 break;
5156 }
5157 }
5158
5159 if (ii == 0) {
5160 ii = i_cal_time_day_of_week (dtstart);
5161 if (ii >= 1)
5162 day_mask |= 1 << (ii - 1);
5163 }
5164
5165 ndays = 0;
5166
5167 for (ii = 0; ii < 7; ii++) {
5168 if ((day_mask & (1 << ii)) != 0)
5169 ndays++;
5170 }
5171
5172 if (prefixed) {
5173 prefix = g_strdup_printf (
5174 g_dngettext (GETTEXT_PACKAGE,
5175 "every week",
5176 "every %d weeks",
5177 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5178 } else {
5179 prefix = g_strdup_printf (
5180 g_dngettext (GETTEXT_PACKAGE,
5181 "Every week",
5182 "Every %d weeks",
5183 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5184 }
5185
5186 for (ii = 0; ii < 7 && ndays; ii++) {
5187 gint bit = ((ii + day_shift) % 7);
5188
5189 if ((day_mask & (1 << bit)) != 0) {
5190 /* Translators: This is used to merge set of week day names in a recurrence, which can contain any
5191 of the week day names. The string always starts with "on DAYNAME", then it can continue either
5192 with ", DAYNAME" or " and DAYNAME", thus it can be something like "on Monday and Tuesday"
5193 or "on Monday, Wednesday and Friday" or simply "on Saturday". The '%1$s' is replaced with
5194 the previously gathered text, while the '%2$s' is replaced with the text to append. */
5195 #define merge_fmt C_("recur-description-dayname", "%1$s%2$s")
5196 #define merge_string(_on_str, _merge_str, _and_str) G_STMT_START {\
5197 if (mid) { \
5198 gchar *tmp; \
5199 if (ndays == 1) \
5200 tmp = g_strdup_printf (merge_fmt, mid, _and_str); \
5201 else \
5202 tmp = g_strdup_printf (merge_fmt, mid, _merge_str); \
5203 g_free (mid); \
5204 mid = tmp; \
5205 } else { \
5206 mid = g_strdup (_on_str); \
5207 } \
5208 ndays--; \
5209 } G_STMT_END
5210 switch (bit) {
5211 case 0:
5212 merge_string (C_("recur-description", "on Sunday"),
5213 C_("recur-description", ", Sunday"),
5214 C_("recur-description", " and Sunday"));
5215 break;
5216 case 1:
5217 merge_string (C_("recur-description", "on Monday"),
5218 C_("recur-description", ", Monday"),
5219 C_("recur-description", " and Monday"));
5220 break;
5221 case 2:
5222 merge_string (C_("recur-description", "on Tuesday"),
5223 C_("recur-description", ", Tuesday"),
5224 C_("recur-description", " and Tuesday"));
5225 break;
5226 case 3:
5227 merge_string (C_("recur-description", "on Wednesday"),
5228 C_("recur-description", ", Wednesday"),
5229 C_("recur-description", " and Wednesday"));
5230 break;
5231 case 4:
5232 merge_string (C_("recur-description", "on Thursday"),
5233 C_("recur-description", ", Thursday"),
5234 C_("recur-description", " and Thursday"));
5235 break;
5236 case 5:
5237 merge_string (C_("recur-description", "on Friday"),
5238 C_("recur-description", ", Friday"),
5239 C_("recur-description", " and Friday"));
5240 break;
5241 case 6:
5242 merge_string (C_("recur-description", "on Saturday"),
5243 C_("recur-description", ", Saturday"),
5244 C_("recur-description", " and Saturday"));
5245 break;
5246 default:
5247 g_warn_if_reached ();
5248 break;
5249 }
5250 #undef merge_string
5251 #undef merge_fmt
5252 }
5253 }
5254 break;
5255 }
5256
5257 case I_CAL_MONTHLY_RECURRENCE: {
5258 enum month_num_options {
5259 MONTH_NUM_INVALID = -1,
5260 MONTH_NUM_FIRST,
5261 MONTH_NUM_SECOND,
5262 MONTH_NUM_THIRD,
5263 MONTH_NUM_FOURTH,
5264 MONTH_NUM_FIFTH,
5265 MONTH_NUM_LAST,
5266 MONTH_NUM_DAY,
5267 MONTH_NUM_OTHER
5268 };
5269
5270 enum month_day_options {
5271 MONTH_DAY_NTH,
5272 MONTH_DAY_MON,
5273 MONTH_DAY_TUE,
5274 MONTH_DAY_WED,
5275 MONTH_DAY_THU,
5276 MONTH_DAY_FRI,
5277 MONTH_DAY_SAT,
5278 MONTH_DAY_SUN
5279 };
5280
5281 gint month_index = 1;
5282 enum month_day_options month_day;
5283 enum month_num_options month_num;
5284
5285 if (n_by_year_day != 0 ||
5286 n_by_week_no != 0 ||
5287 n_by_month != 0 ||
5288 n_by_set_pos > 1)
5289 goto custom;
5290
5291 if (n_by_month_day == 1) {
5292 gint nth;
5293
5294 if (n_by_set_pos != 0)
5295 goto custom;
5296
5297 nth = i_cal_recurrence_get_by_month_day (rrule, 0);
5298 if (nth < 1 && nth != -1)
5299 goto custom;
5300
5301 if (nth == -1) {
5302 month_index = i_cal_time_get_day (dtstart);
5303 month_num = MONTH_NUM_LAST;
5304 } else {
5305 month_index = nth;
5306 month_num = MONTH_NUM_DAY;
5307 }
5308 month_day = MONTH_DAY_NTH;
5309
5310 } else if (n_by_day == 1) {
5311 ICalRecurrenceWeekday weekday;
5312 gint pos;
5313
5314 /* Outlook 2000 uses BYDAY=TU;BYSETPOS=2, and will not
5315 * accept BYDAY=2TU. So we now use the same as Outlook
5316 * by default. */
5317
5318 weekday = i_cal_recurrence_day_day_of_week (i_cal_recurrence_get_by_day (rrule, 0));
5319 pos = i_cal_recurrence_day_position (i_cal_recurrence_get_by_day (rrule, 0));
5320
5321 if (pos == 0) {
5322 if (n_by_set_pos != 1)
5323 goto custom;
5324 pos = i_cal_recurrence_get_by_set_pos (rrule, 0);
5325 } else if (pos < 0) {
5326 goto custom;
5327 }
5328
5329 switch (weekday) {
5330 case I_CAL_MONDAY_WEEKDAY:
5331 month_day = MONTH_DAY_MON;
5332 break;
5333
5334 case I_CAL_TUESDAY_WEEKDAY:
5335 month_day = MONTH_DAY_TUE;
5336 break;
5337
5338 case I_CAL_WEDNESDAY_WEEKDAY:
5339 month_day = MONTH_DAY_WED;
5340 break;
5341
5342 case I_CAL_THURSDAY_WEEKDAY:
5343 month_day = MONTH_DAY_THU;
5344 break;
5345
5346 case I_CAL_FRIDAY_WEEKDAY:
5347 month_day = MONTH_DAY_FRI;
5348 break;
5349
5350 case I_CAL_SATURDAY_WEEKDAY:
5351 month_day = MONTH_DAY_SAT;
5352 break;
5353
5354 case I_CAL_SUNDAY_WEEKDAY:
5355 month_day = MONTH_DAY_SUN;
5356 break;
5357
5358 default:
5359 goto custom;
5360 }
5361
5362 if (pos == -1)
5363 month_num = MONTH_NUM_LAST;
5364 else
5365 month_num = pos - 1;
5366 } else {
5367 goto custom;
5368 }
5369
5370 if (prefixed) {
5371 prefix = g_strdup_printf (
5372 g_dngettext (GETTEXT_PACKAGE,
5373 "every month",
5374 "every %d months",
5375 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5376 } else {
5377 prefix = g_strdup_printf (
5378 g_dngettext (GETTEXT_PACKAGE,
5379 "Every month",
5380 "Every %d months",
5381 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5382 }
5383
5384 switch (month_day) {
5385 case MONTH_DAY_NTH:
5386 if (month_num == MONTH_NUM_LAST) {
5387 switch (month_index) {
5388 case 0:
5389 mid = g_strdup (C_("recur-description", "on the last Sunday"));
5390 break;
5391 case 1:
5392 mid = g_strdup (C_("recur-description", "on the last Monday"));
5393 break;
5394 case 2:
5395 mid = g_strdup (C_("recur-description", "on the last Tuesday"));
5396 break;
5397 case 3:
5398 mid = g_strdup (C_("recur-description", "on the last Wednesday"));
5399 break;
5400 case 4:
5401 mid = g_strdup (C_("recur-description", "on the last Thursday"));
5402 break;
5403 case 5:
5404 mid = g_strdup (C_("recur-description", "on the last Friday"));
5405 break;
5406 case 6:
5407 mid = g_strdup (C_("recur-description", "on the last Saturday"));
5408 break;
5409 default:
5410 g_warning ("%s: What is month_index:%d for the last day?", G_STRFUNC, month_index);
5411 break;
5412 }
5413 } else { /* month_num = MONTH_NUM_DAY; */
5414 switch (month_index) {
5415 case 1:
5416 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5417 mid = g_strdup (C_("recur-description", "on the 1st day"));
5418 break;
5419 case 2:
5420 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5421 mid = g_strdup (C_("recur-description", "on the 2nd day"));
5422 break;
5423 case 3:
5424 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5425 mid = g_strdup (C_("recur-description", "on the 3rd day"));
5426 break;
5427 case 4:
5428 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5429 mid = g_strdup (C_("recur-description", "on the 4th day"));
5430 break;
5431 case 5:
5432 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5433 mid = g_strdup (C_("recur-description", "on the 5th day"));
5434 break;
5435 case 6:
5436 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5437 mid = g_strdup (C_("recur-description", "on the 6th day"));
5438 break;
5439 case 7:
5440 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5441 mid = g_strdup (C_("recur-description", "on the 7th day"));
5442 break;
5443 case 8:
5444 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5445 mid = g_strdup (C_("recur-description", "on the 8th day"));
5446 break;
5447 case 9:
5448 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5449 mid = g_strdup (C_("recur-description", "on the 9th day"));
5450 break;
5451 case 10:
5452 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5453 mid = g_strdup (C_("recur-description", "on the 10th day"));
5454 break;
5455 case 11:
5456 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5457 mid = g_strdup (C_("recur-description", "on the 11th day"));
5458 break;
5459 case 12:
5460 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5461 mid = g_strdup (C_("recur-description", "on the 12th day"));
5462 break;
5463 case 13:
5464 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5465 mid = g_strdup (C_("recur-description", "on the 13th day"));
5466 break;
5467 case 14:
5468 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5469 mid = g_strdup (C_("recur-description", "on the 14th day"));
5470 break;
5471 case 15:
5472 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5473 mid = g_strdup (C_("recur-description", "on the 15th day"));
5474 break;
5475 case 16:
5476 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5477 mid = g_strdup (C_("recur-description", "on the 16th day"));
5478 break;
5479 case 17:
5480 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5481 mid = g_strdup (C_("recur-description", "on the 17th day"));
5482 break;
5483 case 18:
5484 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5485 mid = g_strdup (C_("recur-description", "on the 18th day"));
5486 break;
5487 case 19:
5488 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5489 mid = g_strdup (C_("recur-description", "on the 19th day"));
5490 break;
5491 case 20:
5492 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5493 mid = g_strdup (C_("recur-description", "on the 20th day"));
5494 break;
5495 case 21:
5496 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5497 mid = g_strdup (C_("recur-description", "on the 21st day"));
5498 break;
5499 case 22:
5500 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5501 mid = g_strdup (C_("recur-description", "on the 22nd day"));
5502 break;
5503 case 23:
5504 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5505 mid = g_strdup (C_("recur-description", "on the 23rd day"));
5506 break;
5507 case 24:
5508 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5509 mid = g_strdup (C_("recur-description", "on the 24th day"));
5510 break;
5511 case 25:
5512 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5513 mid = g_strdup (C_("recur-description", "on the 25th day"));
5514 break;
5515 case 26:
5516 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5517 mid = g_strdup (C_("recur-description", "on the 26th day"));
5518 break;
5519 case 27:
5520 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5521 mid = g_strdup (C_("recur-description", "on the 27th day"));
5522 break;
5523 case 28:
5524 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5525 mid = g_strdup (C_("recur-description", "on the 28th day"));
5526 break;
5527 case 29:
5528 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5529 mid = g_strdup (C_("recur-description", "on the 29th day"));
5530 break;
5531 case 30:
5532 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5533 mid = g_strdup (C_("recur-description", "on the 30th day"));
5534 break;
5535 case 31:
5536 /* Translators: This is added to a monthly recurrence, forming something like "Every month on the Xth day" */
5537 mid = g_strdup (C_("recur-description", "on the 31st day"));
5538 break;
5539 }
5540 }
5541 break;
5542 case MONTH_DAY_MON:
5543 switch (month_num) {
5544 case MONTH_NUM_FIRST:
5545 mid = g_strdup (C_("recur-description", "on the first Monday"));
5546 break;
5547 case MONTH_NUM_SECOND:
5548 mid = g_strdup (C_("recur-description", "on the second Monday"));
5549 break;
5550 case MONTH_NUM_THIRD:
5551 mid = g_strdup (C_("recur-description", "on the third Monday"));
5552 break;
5553 case MONTH_NUM_FOURTH:
5554 mid = g_strdup (C_("recur-description", "on the fourth Monday"));
5555 break;
5556 case MONTH_NUM_FIFTH:
5557 mid = g_strdup (C_("recur-description", "on the fifth Monday"));
5558 break;
5559 case MONTH_NUM_LAST:
5560 mid = g_strdup (C_("recur-description", "on the last Monday"));
5561 break;
5562 default:
5563 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5564 break;
5565 }
5566 break;
5567 case MONTH_DAY_TUE:
5568 switch (month_num) {
5569 case MONTH_NUM_FIRST:
5570 mid = g_strdup (C_("recur-description", "on the first Tuesday"));
5571 break;
5572 case MONTH_NUM_SECOND:
5573 mid = g_strdup (C_("recur-description", "on the second Tuesday"));
5574 break;
5575 case MONTH_NUM_THIRD:
5576 mid = g_strdup (C_("recur-description", "on the third Tuesday"));
5577 break;
5578 case MONTH_NUM_FOURTH:
5579 mid = g_strdup (C_("recur-description", "on the fourth Tuesday"));
5580 break;
5581 case MONTH_NUM_FIFTH:
5582 mid = g_strdup (C_("recur-description", "on the fifth Tuesday"));
5583 break;
5584 case MONTH_NUM_LAST:
5585 mid = g_strdup (C_("recur-description", "on the last Tuesday"));
5586 break;
5587 default:
5588 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5589 break;
5590 }
5591 break;
5592 case MONTH_DAY_WED:
5593 switch (month_num) {
5594 case MONTH_NUM_FIRST:
5595 mid = g_strdup (C_("recur-description", "on the first Wednesday"));
5596 break;
5597 case MONTH_NUM_SECOND:
5598 mid = g_strdup (C_("recur-description", "on the second Wednesday"));
5599 break;
5600 case MONTH_NUM_THIRD:
5601 mid = g_strdup (C_("recur-description", "on the third Wednesday"));
5602 break;
5603 case MONTH_NUM_FOURTH:
5604 mid = g_strdup (C_("recur-description", "on the fourth Wednesday"));
5605 break;
5606 case MONTH_NUM_FIFTH:
5607 mid = g_strdup (C_("recur-description", "on the fifth Wednesday"));
5608 break;
5609 case MONTH_NUM_LAST:
5610 mid = g_strdup (C_("recur-description", "on the last Wednesday"));
5611 break;
5612 default:
5613 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5614 break;
5615 }
5616 break;
5617 case MONTH_DAY_THU:
5618 switch (month_num) {
5619 case MONTH_NUM_FIRST:
5620 mid = g_strdup (C_("recur-description", "on the first Thursday"));
5621 break;
5622 case MONTH_NUM_SECOND:
5623 mid = g_strdup (C_("recur-description", "on the second Thursday"));
5624 break;
5625 case MONTH_NUM_THIRD:
5626 mid = g_strdup (C_("recur-description", "on the third Thursday"));
5627 break;
5628 case MONTH_NUM_FOURTH:
5629 mid = g_strdup (C_("recur-description", "on the fourth Thursday"));
5630 break;
5631 case MONTH_NUM_FIFTH:
5632 mid = g_strdup (C_("recur-description", "on the fifth Thursday"));
5633 break;
5634 case MONTH_NUM_LAST:
5635 mid = g_strdup (C_("recur-description", "on the last Thursday"));
5636 break;
5637 default:
5638 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5639 break;
5640 }
5641 break;
5642 case MONTH_DAY_FRI:
5643 switch (month_num) {
5644 case MONTH_NUM_FIRST:
5645 mid = g_strdup (C_("recur-description", "on the first Friday"));
5646 break;
5647 case MONTH_NUM_SECOND:
5648 mid = g_strdup (C_("recur-description", "on the second Friday"));
5649 break;
5650 case MONTH_NUM_THIRD:
5651 mid = g_strdup (C_("recur-description", "on the third Friday"));
5652 break;
5653 case MONTH_NUM_FOURTH:
5654 mid = g_strdup (C_("recur-description", "on the fourth Friday"));
5655 break;
5656 case MONTH_NUM_FIFTH:
5657 mid = g_strdup (C_("recur-description", "on the fifth Friday"));
5658 break;
5659 case MONTH_NUM_LAST:
5660 mid = g_strdup (C_("recur-description", "on the last Friday"));
5661 break;
5662 default:
5663 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5664 break;
5665 }
5666 break;
5667 case MONTH_DAY_SAT:
5668 switch (month_num) {
5669 case MONTH_NUM_FIRST:
5670 mid = g_strdup (C_("recur-description", "on the first Saturday"));
5671 break;
5672 case MONTH_NUM_SECOND:
5673 mid = g_strdup (C_("recur-description", "on the second Saturday"));
5674 break;
5675 case MONTH_NUM_THIRD:
5676 mid = g_strdup (C_("recur-description", "on the third Saturday"));
5677 break;
5678 case MONTH_NUM_FOURTH:
5679 mid = g_strdup (C_("recur-description", "on the fourth Saturday"));
5680 break;
5681 case MONTH_NUM_FIFTH:
5682 mid = g_strdup (C_("recur-description", "on the fifth Saturday"));
5683 break;
5684 case MONTH_NUM_LAST:
5685 mid = g_strdup (C_("recur-description", "on the last Saturday"));
5686 break;
5687 default:
5688 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5689 break;
5690 }
5691 break;
5692 case MONTH_DAY_SUN:
5693 switch (month_num) {
5694 case MONTH_NUM_FIRST:
5695 mid = g_strdup (C_("recur-description", "on the first Sunday"));
5696 break;
5697 case MONTH_NUM_SECOND:
5698 mid = g_strdup (C_("recur-description", "on the second Sunday"));
5699 break;
5700 case MONTH_NUM_THIRD:
5701 mid = g_strdup (C_("recur-description", "on the third Sunday"));
5702 break;
5703 case MONTH_NUM_FOURTH:
5704 mid = g_strdup (C_("recur-description", "on the fourth Sunday"));
5705 break;
5706 case MONTH_NUM_FIFTH:
5707 mid = g_strdup (C_("recur-description", "on the fifth Sunday"));
5708 break;
5709 case MONTH_NUM_LAST:
5710 mid = g_strdup (C_("recur-description", "on the last Sunday"));
5711 break;
5712 default:
5713 g_warning ("%s: What is month_num:%d for month_day:%d?", G_STRFUNC, month_num, month_day);
5714 break;
5715 }
5716 break;
5717 }
5718
5719 break;
5720 }
5721
5722 case I_CAL_YEARLY_RECURRENCE:
5723 if (n_by_day != 0 ||
5724 n_by_month_day != 0 ||
5725 n_by_year_day != 0 ||
5726 n_by_week_no != 0 ||
5727 n_by_month != 0 ||
5728 n_by_set_pos != 0)
5729 goto custom;
5730
5731 if (i_cal_recurrence_get_interval (rrule) > 0) {
5732 if (!i_cal_recurrence_get_count (rrule) && (!until || !i_cal_time_get_year (until))) {
5733 if (prefixed) {
5734 result = g_strdup_printf (
5735 g_dngettext (GETTEXT_PACKAGE,
5736 "every year forever",
5737 "every %d years forever",
5738 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5739 } else {
5740 result = g_strdup_printf (
5741 g_dngettext (GETTEXT_PACKAGE,
5742 "Every year forever",
5743 "Every %d years forever",
5744 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5745 }
5746 } else {
5747 if (prefixed) {
5748 prefix = g_strdup_printf (
5749 g_dngettext (GETTEXT_PACKAGE,
5750 "every year",
5751 "every %d years",
5752 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5753 } else {
5754 prefix = g_strdup_printf (
5755 g_dngettext (GETTEXT_PACKAGE,
5756 "Every year",
5757 "Every %d years",
5758 i_cal_recurrence_get_interval (rrule)), i_cal_recurrence_get_interval (rrule));
5759 }
5760 }
5761 }
5762 break;
5763
5764 default:
5765 break;
5766 }
5767
5768 if (prefix) {
5769 if (i_cal_recurrence_get_count (rrule)) {
5770 suffix = g_strdup_printf (
5771 g_dngettext (GETTEXT_PACKAGE,
5772 /* Translators: This is one of the last possible parts of a recurrence description.
5773 The text is appended at the end of the complete recurrence description, making it
5774 for example: "Every 3 days for 10 occurrences" */
5775 "for one occurrence",
5776 "for %d occurrences",
5777 i_cal_recurrence_get_count (rrule)), i_cal_recurrence_get_count (rrule));
5778 } else if (until && i_cal_time_get_year (until)) {
5779 gchar dt_str[256];
5780
5781 dt_str[0] = 0;
5782
5783 if (!i_cal_time_is_date (until)) {
5784 ICalTimezone *from_zone, *to_zone;
5785
5786 from_zone = i_cal_timezone_get_utc_timezone ();
5787 to_zone = i_cal_time_get_timezone (dtstart);
5788
5789 if (to_zone)
5790 i_cal_time_convert_timezone (until, from_zone, to_zone);
5791
5792 i_cal_time_set_time (until, 0, 0, 0);
5793 i_cal_time_set_is_date (until, TRUE);
5794 }
5795
5796 if (datetime_fmt_func) {
5797 datetime_fmt_func (until, dt_str, 255);
5798 } else {
5799 struct tm tm;
5800
5801 tm = e_cal_util_icaltime_to_tm (until);
5802
5803 e_time_format_date_and_time (&tm, FALSE, FALSE, FALSE, dt_str, 255);
5804 }
5805
5806 if (*dt_str) {
5807 /* Translators: This is one of the last possible parts of a recurrence description.
5808 The '%s' is replaced with actual date, thus it can create something like
5809 "until Mon 15.1.2018". The text is appended at the end of the complete
5810 recurrence description, making it for example: "Every 3 days until Mon 15.1.2018" */
5811 suffix = g_strdup_printf (C_("recur-description", "until %s"), dt_str);
5812 }
5813 } else {
5814 /* Translators: This is one of the last possible parts of a recurrence description.
5815 The text is appended at the end of the complete recurrence description, making it
5816 for example: "Every 2 months on Tuesday, Thursday and Friday forever" */
5817 suffix = g_strdup (C_("recur-description", "forever"));
5818 }
5819 }
5820
5821 custom:
5822 if (!result && prefix && suffix) {
5823 if (mid) {
5824 /* Translators: This constructs a complete recurrence description; the '%1$s' is like "Every 2 weeks",
5825 the '%2$s' is like "on Tuesday and Friday" and the '%3$s' is like "for 10 occurrences", constructing
5826 together one sentence: "Every 2 weeks on Tuesday and Friday for 10 occurrences". */
5827 result = g_strdup_printf (C_("recur-description", "%1$s %2$s %3$s"), prefix, mid, suffix);
5828 } else {
5829 /* Translators: This constructs a complete recurrence description; the '%1$s' is like "Every 2 days",
5830 the '%2$s' is like "for 10 occurrences", constructing together one sentence:
5831 "Every 2 days for 10 occurrences". */
5832 result = g_strdup_printf (C_("recur-description", "%1$s %2$s"), prefix, suffix);
5833 }
5834 }
5835
5836 if (result) {
5837 gint n_exdates;
5838 gchar *tmp;
5839
5840 n_exdates = i_cal_component_count_properties (icalcomp, I_CAL_EXDATE_PROPERTY);
5841 if (n_exdates > 0) {
5842 gchar *exdates_str;
5843
5844 exdates_str = g_strdup_printf (
5845 g_dngettext (GETTEXT_PACKAGE,
5846 /* Translators: This text is appended at the end of complete recur description using "%s%s" in
5847 context "recur-description" */
5848 ", with one exception",
5849 ", with %d exceptions",
5850 n_exdates), n_exdates);
5851
5852 /* Translators: This appends text like ", with 3 exceptions" at the end of complete recurrence description.
5853 The "%1$s" is replaced with the recurrence description, the "%2$s" with the text about exceptions.
5854 It will form something like: "Every 2 weeks on Tuesday and Friday for 10 occurrences, with 3 exceptions" */
5855 tmp = g_strdup_printf (C_("recur-description", "%1$s%2$s"), result, exdates_str);
5856
5857 g_free (exdates_str);
5858 g_free (result);
5859 result = tmp;
5860 }
5861
5862 if (prefixed) {
5863 const gchar *comp_prefix;
5864
5865 if (i_cal_component_isa (icalcomp) == I_CAL_VEVENT_COMPONENT) {
5866 if (i_cal_component_count_properties (icalcomp, I_CAL_ORGANIZER_PROPERTY) > 0 &&
5867 i_cal_component_count_properties (icalcomp, I_CAL_ATTENDEE_PROPERTY) > 0) {
5868 comp_prefix = C_("recur-description", "The meeting recurs");
5869 } else {
5870 comp_prefix = C_("recur-description", "The appointment recurs");
5871 }
5872 } else if (i_cal_component_isa (icalcomp) == I_CAL_VTODO_COMPONENT) {
5873 comp_prefix = C_("recur-description", "The task recurs");
5874 } else /* if (i_cal_component_isa (comp) == I_CAL_VJOURNAL_COMPONENT) */ {
5875 comp_prefix = C_("recur-description", "The memo recurs");
5876 }
5877
5878 /* Translators: This adds a prefix in front of the complete recurrence description.
5879 The '%1$s' is replaced with something like "The meeting recurs" and
5880 the '%2$s' with something like "every 2 days forever", thus forming
5881 sentence like "This meeting recurs every 2 days forever" */
5882 tmp = g_strdup_printf (C_("recur-description-prefix", "%1$s %2$s"), comp_prefix, result);
5883
5884 g_free (result);
5885 result = tmp;
5886 }
5887 } else if (fallback) {
5888 if (i_cal_component_isa (icalcomp) == I_CAL_VEVENT_COMPONENT) {
5889 if (i_cal_component_count_properties (icalcomp, I_CAL_ORGANIZER_PROPERTY) > 0 &&
5890 i_cal_component_count_properties (icalcomp, I_CAL_ATTENDEE_PROPERTY) > 0) {
5891 result = g_strdup (C_("recur-description", "The meeting recurs"));
5892 } else {
5893 result = g_strdup (C_("recur-description", "The appointment recurs"));
5894 }
5895 } else if (i_cal_component_isa (icalcomp) == I_CAL_VTODO_COMPONENT) {
5896 result = g_strdup (C_("recur-description", "The task recurs"));
5897 } else if (i_cal_component_isa (icalcomp) == I_CAL_VJOURNAL_COMPONENT) {
5898 result = g_strdup (C_("recur-description", "The memo recurs"));
5899 }
5900 }
5901
5902 g_clear_object (&dtstart);
5903 g_clear_object (&until);
5904 g_clear_object (&rrule);
5905 g_clear_object (&prop);
5906 g_free (prefix);
5907 g_free (mid);
5908 g_free (suffix);
5909
5910 return result;
5911 }
5912
5913 /**
5914 * e_cal_recur_describe_recurrence:
5915 * @icalcomp: an #ICalComponent
5916 * @week_start_day: a day when the week starts
5917 * @flags: bit-or of #ECalRecurDescribeRecurrenceFlags
5918 *
5919 * Describes some simple types of recurrences in a human-readable and localized way.
5920 * The @flags influence the output format and what to do when the @icalcomp
5921 * contains more complicated recurrence, some which the function cannot describe.
5922 *
5923 * The @week_start_day is used for weekly recurrences, to start the list of selected
5924 * days at that day.
5925 *
5926 * Uses e_time_format_date_and_time() to format the date/time value in the string.
5927 * Call e_cal_recur_describe_recurrence_ex() with a custom formatting function.
5928 *
5929 * Free the returned string with g_free(), when no longer needed.
5930 *
5931 * Returns: (nullable) (transfer full): a newly allocated string, which
5932 * describes the recurrence of the @icalcomp, or #NULL, when the @icalcomp
5933 * doesn't recur or the recurrence is too complicated to describe, also
5934 * according to given @flags.
5935 *
5936 * Since: 3.30
5937 **/
5938 gchar *
e_cal_recur_describe_recurrence(ICalComponent * icalcomp,GDateWeekday week_start_day,guint32 flags)5939 e_cal_recur_describe_recurrence (ICalComponent *icalcomp,
5940 GDateWeekday week_start_day,
5941 guint32 flags)
5942 {
5943 return e_cal_recur_describe_recurrence_ex (icalcomp, week_start_day, flags, NULL);
5944 }
5945