Line data Source code
1 : /* Copyright (C) 2005, Chris Shoemaker <c.shoemaker@cox.net>
2 : *
3 : * This program is free software; you can redistribute it and/or
4 : * modify it under the terms of the GNU General Public License as
5 : * published by the Free Software Foundation; either version 2 of
6 : * the License, or (at your option) any later version.
7 : *
8 : * This program is distributed in the hope that it will be useful,
9 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 : * GNU General Public License for more details.
12 : *
13 : * You should have received a copy of the GNU General Public License
14 : * along with this program; if not, contact:
15 : *
16 : * Free Software Foundation Voice: +1-617-542-5942
17 : * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
18 : * Boston, MA 02110-1301, USA gnu@gnu.org
19 : */
20 :
21 : #include <config.h>
22 : #include <time.h>
23 : #include <glib.h>
24 : #include <glib/gi18n.h>
25 : #include <string.h>
26 : #include <stdint.h>
27 : #include "Recurrence.h"
28 : #include "gnc-date.h"
29 : #include "qof.h"
30 : #include "gnc-engine.h"
31 : #include "gnc-date.h"
32 : #include "Account.h"
33 : #include <stdint.h>
34 : #include <gnc-glib-utils.h>
35 :
36 : #define LOG_MOD "gnc.engine.recurrence"
37 : static QofLogModule log_module = LOG_MOD;
38 : #undef G_LOG_DOMAIN
39 : #define G_LOG_DOMAIN LOG_MOD
40 :
41 : static GDate invalid_gdate;
42 :
43 : /* Do not intl. These are used for xml storage. */
44 : static const gchar *period_type_strings[NUM_PERIOD_TYPES] =
45 : {
46 : "once", "day", "week", "month", "end of month",
47 : "nth weekday", "last weekday", "year",
48 : };
49 : static const gchar *weekend_adj_strings[NUM_WEEKEND_ADJS] =
50 : {
51 : "none", "back", "forward",
52 : };
53 :
54 : #define VALID_PERIOD_TYPE(pt) ((0 <= (pt)) && ((pt) < NUM_PERIOD_TYPES))
55 : #define VALID_WEEKEND_ADJ(wadj) ((0 <= (wadj)) && ((wadj) < NUM_WEEKEND_ADJS))
56 :
57 : PeriodType
58 21600006 : recurrenceGetPeriodType(const Recurrence *r)
59 : {
60 21600006 : return r ? r->ptype : PERIOD_INVALID;
61 : }
62 :
63 : guint
64 21600002 : recurrenceGetMultiplier(const Recurrence *r)
65 : {
66 21600002 : return r ? r->mult : 0;
67 : }
68 :
69 : GDate
70 21600006 : recurrenceGetDate(const Recurrence *r)
71 : {
72 21600006 : return r ? r->start : invalid_gdate;
73 : }
74 :
75 : time64
76 0 : recurrenceGetTime(const Recurrence *r)
77 : {
78 0 : return r ? gdate_to_time64(r->start) : INT64_MAX;
79 : }
80 :
81 : WeekendAdjust
82 21600006 : recurrenceGetWeekendAdjust(const Recurrence *r)
83 : {
84 21600006 : return r ? r->wadj : WEEKEND_ADJ_INVALID;
85 : }
86 :
87 : void
88 21600061 : recurrenceSet(Recurrence *r, guint16 mult, PeriodType pt, const GDate *_start, WeekendAdjust wadj)
89 : {
90 21600061 : r->ptype = VALID_PERIOD_TYPE(pt) ? pt : PERIOD_MONTH;
91 21600061 : r->mult = (pt == PERIOD_ONCE) ? 0 : (mult > 0 ? mult : 1);
92 :
93 21600061 : if (_start && g_date_valid(_start))
94 : {
95 21600061 : r->start = *_start;
96 : }
97 : else
98 : {
99 0 : gnc_gdate_set_today (&r->start);
100 : }
101 :
102 : /* Some of the unusual period types also specify phase. For those
103 : types, we ensure that the start date agrees with that phase. */
104 21600061 : switch (r->ptype)
105 : {
106 2700003 : case PERIOD_END_OF_MONTH:
107 2700003 : g_date_set_day(&r->start, g_date_get_days_in_month
108 2700003 : (g_date_get_month(&r->start),
109 2700003 : g_date_get_year(&r->start)));
110 2700003 : break;
111 2700005 : case PERIOD_LAST_WEEKDAY:
112 : {
113 : GDateDay dim;
114 2700005 : dim = g_date_get_days_in_month(g_date_get_month(&r->start),
115 2700005 : g_date_get_year(&r->start));
116 7272005 : while (dim - g_date_get_day(&r->start) >= 7)
117 4572000 : g_date_add_days(&r->start, 7);
118 : }
119 2700005 : break;
120 2700006 : case PERIOD_NTH_WEEKDAY:
121 2700006 : if ((g_date_get_day(&r->start) - 1) / 7 == 4) /* Fifth week */
122 189000 : r->ptype = PERIOD_LAST_WEEKDAY;
123 2700006 : break;
124 13500047 : default:
125 13500047 : break;
126 : }
127 :
128 21600061 : switch (r->ptype)
129 : {
130 8100033 : case PERIOD_MONTH:
131 : case PERIOD_END_OF_MONTH:
132 : case PERIOD_YEAR:
133 8100033 : r->wadj = wadj;
134 8100033 : break;
135 13500028 : default:
136 13500028 : r->wadj = WEEKEND_ADJ_NONE;
137 13500028 : break;
138 : }
139 21600061 : }
140 :
141 : /* nth_weekday_compare() is a helper function for the
142 : PERIOD_{NTH,LAST}_WEEKDAY case. It returns the offset, in days,
143 : from 'next' to the nth weekday specified by the 'start' date (and
144 : the period type), in the same month as 'next'. A negative offset
145 : means earlier than 'next'; a zero offset means 'next' *is* the nth
146 : weekday in that month; a positive offset means later than
147 : 'next'. */
148 : static gint
149 19 : nth_weekday_compare(const GDate *start, const GDate *next, PeriodType pt)
150 : {
151 : GDateDay sd, nd;
152 : gint matchday, dim, week;
153 :
154 19 : nd = g_date_get_day(next);
155 19 : sd = g_date_get_day(start);
156 19 : week = sd / 7 > 3 ? 3 : sd / 7;
157 19 : if (week > 0 && sd % 7 == 0 && sd != 28)
158 4 : --week;
159 : /* matchday has a week part, capped at 3 weeks, and a day part,
160 : capped at 7 days, so max(matchday) == 3*7 + 7 == 28. */
161 38 : matchday = 7 * week + //((sd - 1) / 7 == 4 ? 3 : (sd - 1) / 7) +
162 19 : (nd - g_date_get_weekday(next) + g_date_get_weekday(start) + 7) % 7;
163 : /* That " + 7" is to avoid negative modulo in case nd < 6. */
164 :
165 19 : dim = g_date_get_days_in_month(
166 19 : g_date_get_month(next), g_date_get_year(next));
167 19 : if ((dim - matchday) >= 7 && pt == PERIOD_LAST_WEEKDAY)
168 3 : matchday += 7; /* Go to the fifth week, if needed */
169 19 : if (pt == PERIOD_NTH_WEEKDAY && (matchday % 7 == 0))
170 0 : matchday += 7;
171 :
172 19 : return matchday - nd; /* Offset from 'next' to matchday */
173 : }
174 :
175 :
176 21609960 : static void adjust_for_weekend(PeriodType pt, WeekendAdjust wadj, GDate *date)
177 : {
178 21609960 : if (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH)
179 : {
180 8109863 : if (g_date_get_weekday(date) == G_DATE_SATURDAY || g_date_get_weekday(date) == G_DATE_SUNDAY)
181 : {
182 2084092 : switch (wadj)
183 : {
184 693000 : case WEEKEND_ADJ_BACK:
185 693000 : g_date_subtract_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 1 : 2);
186 693000 : break;
187 693000 : case WEEKEND_ADJ_FORWARD:
188 693000 : g_date_add_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 2 : 1);
189 693000 : break;
190 698092 : case WEEKEND_ADJ_NONE:
191 : default:
192 698092 : break;
193 : }
194 : }
195 : }
196 21609960 : }
197 :
198 : /* This is the only real algorithm related to recurrences. It goes:
199 : Step 1) Go forward one period from the reference date.
200 : Step 2) Back up to align to the phase of the start date.
201 : */
202 : void
203 21605018 : recurrenceNextInstance(const Recurrence *r, const GDate *ref, GDate *next)
204 : {
205 : PeriodType pt;
206 : const GDate *start;
207 : GDate adjusted_start;
208 : guint mult;
209 : WeekendAdjust wadj;
210 :
211 43205066 : g_return_if_fail(r);
212 21605018 : g_return_if_fail(ref);
213 21605018 : g_return_if_fail(g_date_valid(&r->start));
214 21605018 : g_return_if_fail(g_date_valid(ref));
215 :
216 21605018 : start = &r->start;
217 21605018 : mult = r->mult;
218 21605018 : pt = r->ptype;
219 21605018 : wadj = r->wadj;
220 : /* If the ref date comes before the start date then the next
221 : occurrence is always the start date, and we're done. */
222 : // However, it's possible for the start date to fall on an exception (a weekend), in that case, it needs to be corrected.
223 21605018 : adjusted_start = *start;
224 21605018 : adjust_for_weekend(pt,wadj,&adjusted_start);
225 21605018 : if (g_date_compare(ref, &adjusted_start) < 0)
226 : {
227 21600038 : g_date_set_julian(next, g_date_get_julian(&adjusted_start));
228 21600038 : return;
229 : }
230 4980 : g_date_set_julian(next, g_date_get_julian(ref)); /* start at refDate */
231 :
232 : /* Step 1: move FORWARD one period, passing exactly one occurrence. */
233 4980 : switch (pt)
234 : {
235 6 : case PERIOD_YEAR:
236 6 : mult *= 12;
237 : /* fall through */
238 4942 : case PERIOD_MONTH:
239 : case PERIOD_NTH_WEEKDAY:
240 : case PERIOD_LAST_WEEKDAY:
241 : case PERIOD_END_OF_MONTH:
242 : /* Takes care of short months. */
243 0 : if (r->wadj == WEEKEND_ADJ_BACK &&
244 4942 : (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH) &&
245 0 : (g_date_get_weekday(next) == G_DATE_SATURDAY || g_date_get_weekday(next) == G_DATE_SUNDAY))
246 : {
247 : /* Allows the following Friday-based calculations to proceed if 'next'
248 : is between Friday and the target day. */
249 0 : g_date_subtract_days(next, g_date_get_weekday(next) == G_DATE_SATURDAY ? 1 : 2);
250 : }
251 0 : if (r->wadj == WEEKEND_ADJ_BACK &&
252 4942 : (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH) &&
253 0 : g_date_get_weekday(next) == G_DATE_FRIDAY)
254 : {
255 : GDate tmp_sat;
256 : GDate tmp_sun;
257 0 : g_date_set_julian(&tmp_sat, g_date_get_julian(next));
258 0 : g_date_set_julian(&tmp_sun, g_date_get_julian(next));
259 0 : g_date_add_days(&tmp_sat, 1);
260 0 : g_date_add_days(&tmp_sun, 2);
261 :
262 0 : if (pt == PERIOD_END_OF_MONTH)
263 : {
264 0 : if (g_date_is_last_of_month(next) ||
265 0 : g_date_is_last_of_month(&tmp_sat) ||
266 0 : g_date_is_last_of_month(&tmp_sun))
267 0 : g_date_add_months(next, mult);
268 : else
269 : /* one fewer month fwd because of the occurrence in this month */
270 0 : g_date_add_months(next, mult - 1);
271 : }
272 : else
273 : {
274 0 : if (g_date_get_day(&tmp_sat) == g_date_get_day(start))
275 : {
276 0 : g_date_add_days(next, 1);
277 0 : g_date_add_months(next, mult);
278 : }
279 0 : else if (g_date_get_day(&tmp_sun) == g_date_get_day(start))
280 : {
281 0 : g_date_add_days(next, 2);
282 0 : g_date_add_months(next, mult);
283 : }
284 0 : else if (g_date_get_day(next) >= g_date_get_day(start))
285 : {
286 0 : g_date_add_months(next, mult);
287 : }
288 0 : else if (g_date_is_last_of_month(next))
289 : {
290 0 : g_date_add_months(next, mult);
291 : }
292 0 : else if (g_date_is_last_of_month(&tmp_sat))
293 : {
294 0 : g_date_add_days(next, 1);
295 0 : g_date_add_months(next, mult);
296 : }
297 0 : else if (g_date_is_last_of_month(&tmp_sun))
298 : {
299 0 : g_date_add_days(next, 2);
300 0 : g_date_add_months(next, mult);
301 : }
302 : else
303 : {
304 : /* one fewer month fwd because of the occurrence in this month */
305 0 : g_date_add_months(next, mult - 1);
306 : }
307 : }
308 : }
309 9877 : else if ( g_date_is_last_of_month(next) ||
310 13 : ((pt == PERIOD_MONTH || pt == PERIOD_YEAR) &&
311 9890 : g_date_get_day(next) >= g_date_get_day(start)) ||
312 7 : ((pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY) &&
313 8 : nth_weekday_compare(start, next, pt) <= 0) )
314 4934 : g_date_add_months(next, mult);
315 : else
316 : /* one fewer month fwd because of the occurrence in this month */
317 8 : g_date_add_months(next, mult - 1);
318 4942 : break;
319 0 : case PERIOD_WEEK:
320 0 : mult *= 7;
321 : /* fall through */
322 28 : case PERIOD_DAY:
323 28 : g_date_add_days(next, mult);
324 28 : break;
325 10 : case PERIOD_ONCE:
326 10 : g_date_clear(next, 1); /* We already caught the case where ref is */
327 10 : return; /* earlier than start, so this is invalid. */
328 0 : default:
329 0 : PERR("Invalid period type");
330 0 : break;
331 : }
332 :
333 : /* Step 2: Back up to align to the base phase. To ensure forward
334 : progress, we never subtract as much as we added (x % mult < mult). */
335 4970 : switch (pt)
336 : {
337 4942 : case PERIOD_YEAR:
338 : case PERIOD_MONTH:
339 : case PERIOD_NTH_WEEKDAY:
340 : case PERIOD_LAST_WEEKDAY:
341 : case PERIOD_END_OF_MONTH:
342 : {
343 : guint dim, n_months;
344 :
345 4942 : n_months = 12 * (g_date_get_year(next) - g_date_get_year(start)) +
346 4942 : (g_date_get_month(next) - g_date_get_month(start));
347 4942 : g_date_subtract_months(next, n_months % mult);
348 :
349 : /* Ok, now we're in the right month, so we just have to align
350 : the day in one of the three possible ways. */
351 4942 : dim = g_date_get_days_in_month(g_date_get_month(next),
352 4942 : g_date_get_year(next));
353 4942 : if (pt == PERIOD_LAST_WEEKDAY || pt == PERIOD_NTH_WEEKDAY)
354 : {
355 11 : gint wdresult = nth_weekday_compare(start, next, pt);
356 11 : if (wdresult < 0)
357 : {
358 5 : wdresult = -wdresult;
359 5 : g_date_subtract_days(next, wdresult);
360 : }
361 : else
362 6 : g_date_add_days(next, wdresult);
363 11 : }
364 4931 : else if (pt == PERIOD_END_OF_MONTH || g_date_get_day(start) >= dim)
365 3 : g_date_set_day(next, dim); /* last day in the month */
366 : else
367 4928 : g_date_set_day(next, g_date_get_day(start)); /*same day as start*/
368 :
369 : /* Adjust for dates on the weekend. */
370 4942 : adjust_for_weekend(pt,wadj,next);
371 : }
372 4942 : break;
373 28 : case PERIOD_WEEK:
374 : case PERIOD_DAY:
375 28 : g_date_subtract_days(next, g_date_days_between(start, next) % mult);
376 28 : break;
377 0 : default:
378 0 : PERR("Invalid period type");
379 0 : break;
380 : }
381 : }
382 :
383 : /* Zero-based index */
384 : void
385 1914 : recurrenceNthInstance(const Recurrence *r, guint n, GDate *date)
386 : {
387 : GDate ref;
388 : guint i;
389 :
390 6831 : for (*date = ref = r->start, i = 0; i < n; i++)
391 : {
392 4917 : recurrenceNextInstance(r, &ref, date);
393 4917 : ref = *date;
394 : }
395 1914 : }
396 :
397 : time64
398 1914 : recurrenceGetPeriodTime(const Recurrence *r, guint period_num, gboolean end)
399 : {
400 : GDate date;
401 : time64 time;
402 1914 : recurrenceNthInstance(r, period_num + (end ? 1 : 0), &date);
403 1914 : if (end)
404 : {
405 847 : g_date_subtract_days(&date, 1);
406 847 : time = gnc_dmy2time64_end (g_date_get_day(&date),
407 847 : g_date_get_month(&date),
408 847 : g_date_get_year (&date));
409 :
410 : }
411 : else
412 : {
413 1067 : time = gnc_dmy2time64 (g_date_get_day(&date),
414 1067 : g_date_get_month(&date),
415 1067 : g_date_get_year (&date));
416 : }
417 1914 : return time;
418 : }
419 :
420 : gnc_numeric
421 549 : recurrenceGetAccountPeriodValue(const Recurrence *r, Account *acc, guint n)
422 : {
423 : time64 t1, t2;
424 :
425 : // FIXME: maybe zero is not best error return val.
426 549 : g_return_val_if_fail(r && acc, gnc_numeric_zero());
427 549 : t1 = recurrenceGetPeriodTime(r, n, FALSE);
428 549 : t2 = recurrenceGetPeriodTime(r, n, TRUE);
429 549 : return xaccAccountGetNoclosingBalanceChangeInCurrencyForPeriod (acc, t1, t2, TRUE);
430 : }
431 :
432 : void
433 74 : recurrenceListNextInstance(const GList *rlist, const GDate *ref, GDate *next)
434 : {
435 : const GList *iter;
436 : GDate nextSingle; /* The next date for an individual recurrence */
437 :
438 74 : g_date_clear(next, 1);
439 :
440 : // empty rlist = no recurrence
441 74 : if (rlist == NULL)
442 : {
443 0 : return;
444 : }
445 :
446 74 : g_return_if_fail(ref && next && g_date_valid(ref));
447 :
448 148 : for (iter = rlist; iter; iter = iter->next)
449 : {
450 74 : auto r = static_cast<const Recurrence *>(iter->data);
451 :
452 74 : recurrenceNextInstance(r, ref, &nextSingle);
453 74 : if (!g_date_valid(&nextSingle)) continue;
454 :
455 64 : if (g_date_valid(next))
456 0 : g_date_order(next, &nextSingle); /* swaps dates if needed */
457 : else
458 64 : *next = nextSingle; /* first date is always earliest so far */
459 : }
460 : }
461 :
462 : /* Caller owns the returned memory */
463 : gchar *
464 10 : recurrenceToString(const Recurrence *r)
465 : {
466 : gchar *tmpDate, *ret;
467 : const gchar *tmpPeriod;
468 :
469 10 : g_return_val_if_fail(g_date_valid(&r->start), NULL);
470 10 : tmpDate = g_new0(gchar, MAX_DATE_LENGTH + 1);
471 10 : g_date_strftime(tmpDate, MAX_DATE_LENGTH, "%x", &r->start);
472 :
473 10 : if (r->ptype == PERIOD_ONCE)
474 : {
475 0 : ret = g_strdup_printf("once on %s", tmpDate);
476 0 : goto done;
477 : }
478 :
479 10 : tmpPeriod = period_type_strings[r->ptype];
480 10 : if (r->mult > 1)
481 0 : ret = g_strdup_printf("Every %d %ss beginning %s",
482 0 : r->mult, tmpPeriod, tmpDate);
483 : else
484 10 : ret = g_strdup_printf("Every %s beginning %s",
485 : tmpPeriod, tmpDate);
486 10 : done:
487 10 : g_free(tmpDate);
488 10 : return ret;
489 : }
490 :
491 : /* caller owns the returned memory */
492 : gchar *
493 5 : recurrenceListToString(const GList *r)
494 : {
495 : const GList *iter;
496 : GString *str;
497 : gchar *s;
498 :
499 5 : str = g_string_new("");
500 5 : if (r == NULL)
501 : {
502 0 : g_string_append(str, _("None"));
503 : }
504 : else
505 : {
506 10 : for (iter = r; iter; iter = iter->next)
507 : {
508 5 : if (iter != r)
509 : {
510 : /* Translators: " + " is an separator in a list of string-representations of recurrence frequencies */
511 0 : g_string_append(str, _(" + "));
512 : }
513 5 : s = recurrenceToString((Recurrence *)iter->data);
514 : g_string_append(str, s);
515 5 : g_free(s);
516 : }
517 : }
518 5 : return g_string_free(str, FALSE);
519 : }
520 :
521 : const gchar *
522 6 : recurrencePeriodTypeToString(PeriodType pt)
523 : {
524 6 : return VALID_PERIOD_TYPE(pt) ? period_type_strings[pt] : NULL;
525 : }
526 :
527 : PeriodType
528 10 : recurrencePeriodTypeFromString(const gchar *str)
529 : {
530 : int i;
531 :
532 40 : for (i = 0; i < NUM_PERIOD_TYPES; i++)
533 40 : if (g_strcmp0(period_type_strings[i], str) == 0)
534 10 : return static_cast<PeriodType>(i);
535 0 : return static_cast<PeriodType>(-1);
536 : }
537 :
538 : const gchar *
539 6 : recurrenceWeekendAdjustToString(WeekendAdjust wadj)
540 : {
541 6 : return VALID_WEEKEND_ADJ(wadj) ? weekend_adj_strings[wadj] : NULL;
542 : }
543 :
544 : WeekendAdjust
545 6 : recurrenceWeekendAdjustFromString(const gchar *str)
546 : {
547 : int i;
548 :
549 10 : for (i = 0; i < NUM_WEEKEND_ADJS; i++)
550 10 : if (g_strcmp0(weekend_adj_strings[i], str) == 0)
551 6 : return static_cast<WeekendAdjust>(i);
552 0 : return static_cast<WeekendAdjust>(-1);
553 : }
554 :
555 : gboolean
556 0 : recurrenceListIsSemiMonthly(GList *recurrences)
557 : {
558 0 : if (gnc_list_length_cmp (recurrences, 2))
559 0 : return FALSE;
560 :
561 : // should be a "semi-monthly":
562 : {
563 0 : Recurrence *first = (Recurrence*)g_list_nth_data(recurrences, 0);
564 0 : Recurrence *second = (Recurrence*)g_list_nth_data(recurrences, 1);
565 : PeriodType first_period, second_period;
566 0 : first_period = recurrenceGetPeriodType(first);
567 0 : second_period = recurrenceGetPeriodType(second);
568 :
569 0 : if (!((first_period == PERIOD_MONTH
570 0 : || first_period == PERIOD_END_OF_MONTH
571 0 : || first_period == PERIOD_LAST_WEEKDAY)
572 : && (second_period == PERIOD_MONTH
573 0 : || second_period == PERIOD_END_OF_MONTH
574 0 : || second_period == PERIOD_LAST_WEEKDAY)))
575 : {
576 : /*g_error("unknown 2-recurrence composite with period_types first [%d] second [%d]",
577 : first_period, second_periodD);*/
578 0 : return FALSE;
579 : }
580 : }
581 0 : return TRUE;
582 : }
583 :
584 : gboolean
585 0 : recurrenceListIsWeeklyMultiple(const GList *recurrences)
586 : {
587 : const GList *r_iter;
588 :
589 0 : for (r_iter = recurrences; r_iter != NULL; r_iter = r_iter->next)
590 : {
591 0 : Recurrence *r = (Recurrence*)r_iter->data;
592 0 : if (recurrenceGetPeriodType(r) != PERIOD_WEEK)
593 : {
594 0 : return FALSE;
595 : }
596 : }
597 0 : return TRUE;
598 : }
599 :
600 : static void
601 0 : _weekly_list_to_compact_string(GList *rs, GString *buf)
602 : {
603 : int dow_idx;
604 0 : char dow_present_bits = 0;
605 0 : int multiplier = -1;
606 0 : for (; rs != NULL; rs = rs->next)
607 : {
608 0 : Recurrence *r = (Recurrence*)rs->data;
609 0 : GDate date = recurrenceGetDate(r);
610 0 : GDateWeekday dow = g_date_get_weekday(&date);
611 0 : if (dow == G_DATE_BAD_WEEKDAY)
612 : {
613 0 : g_critical("bad weekday pretty-printing recurrence");
614 0 : continue;
615 : }
616 0 : dow_present_bits |= (1 << (dow % 7));
617 :
618 : // there's not necessarily a single multiplier, but for all intents
619 : // and purposes this will be fine.
620 0 : multiplier = recurrenceGetMultiplier(r);
621 : }
622 0 : g_string_printf(buf, "%s", _("Weekly"));
623 0 : if (multiplier > 1)
624 : {
625 : /* Translators: %u is the recurrence multiplier, i.e. this
626 : event should occur every %u'th week. */
627 0 : g_string_append_printf(buf, _(" (x%u)"), multiplier);
628 : }
629 0 : g_string_append_printf(buf, ": ");
630 :
631 : // @@fixme: this is only Sunday-started weeks. :/
632 0 : for (dow_idx = 0; dow_idx < 7; dow_idx++)
633 : {
634 0 : if ((dow_present_bits & (1 << dow_idx)) != 0)
635 : {
636 : gchar dbuf[10];
637 0 : gnc_dow_abbrev(dbuf, 10, dow_idx);
638 0 : g_string_append_unichar(buf, g_utf8_get_char(dbuf));
639 : }
640 : else
641 : {
642 0 : g_string_append_printf(buf, "-");
643 : }
644 : }
645 0 : }
646 :
647 : /* A constant is needed for the array size */
648 : #define abbrev_day_name_bufsize 10
649 : static void
650 0 : _monthly_append_when(Recurrence *r, GString *buf)
651 : {
652 0 : GDate date = recurrenceGetDate(r);
653 0 : if (recurrenceGetPeriodType(r) == PERIOD_LAST_WEEKDAY)
654 : {
655 : gchar day_name_buf[abbrev_day_name_bufsize];
656 :
657 0 : gnc_dow_abbrev(day_name_buf, abbrev_day_name_bufsize, g_date_get_weekday(&date) % 7);
658 :
659 : /* Translators: %s is an already-localized form of the day of the week. */
660 0 : g_string_append_printf(buf, _("last %s"), day_name_buf);
661 : }
662 0 : else if (recurrenceGetPeriodType(r) == PERIOD_NTH_WEEKDAY)
663 : {
664 0 : int week = 0;
665 0 : int day_of_month_index = 0;
666 0 : const char *numerals[] = {N_("1st"), N_("2nd"), N_("3rd"), N_("4th")};
667 : gchar day_name_buf[abbrev_day_name_bufsize];
668 :
669 0 : gnc_dow_abbrev(day_name_buf, abbrev_day_name_bufsize, g_date_get_weekday(&date) % 7);
670 0 : day_of_month_index = g_date_get_day(&date) - 1;
671 0 : week = day_of_month_index / 7 > 3 ? 3 : day_of_month_index / 7;
672 : /* Translators: %s is the string 1st, 2nd, 3rd and so on, and
673 : %s is an already-localized form of the day of the week. */
674 0 : g_string_append_printf(buf, _("%s %s"), _(numerals[week]), day_name_buf);
675 : }
676 : else
677 : {
678 : /* Translators: %u is the day of month */
679 0 : g_string_append_printf(buf, "%u", g_date_get_day(&date));
680 : }
681 0 : }
682 :
683 : gchar*
684 0 : recurrenceListToCompactString(GList *rs)
685 : {
686 0 : GString *buf = g_string_sized_new(16);
687 0 : gint rs_len = g_list_length (rs);
688 :
689 0 : if (rs_len == 0)
690 : {
691 0 : g_string_printf(buf, "%s", _("None"));
692 0 : goto rtn;
693 : }
694 :
695 0 : if (rs_len > 1)
696 : {
697 0 : if (recurrenceListIsWeeklyMultiple(rs))
698 : {
699 0 : _weekly_list_to_compact_string(rs, buf);
700 : }
701 0 : else if (recurrenceListIsSemiMonthly(rs))
702 : {
703 : Recurrence *first, *second;
704 0 : first = (Recurrence*)g_list_nth_data(rs, 0);
705 0 : second = (Recurrence*)g_list_nth_data(rs, 1);
706 0 : if (recurrenceGetMultiplier(first) != recurrenceGetMultiplier(second))
707 : {
708 0 : g_warning("lying about non-equal semi-monthly recurrence multiplier: %d vs. %d",
709 : recurrenceGetMultiplier(first), recurrenceGetMultiplier(second));
710 : }
711 :
712 0 : g_string_printf(buf, "%s", _("Semi-monthly"));
713 0 : g_string_append_printf(buf, " ");
714 0 : if (recurrenceGetMultiplier(first) > 1)
715 : {
716 : /* Translators: %u is the recurrence multiplier number */
717 0 : g_string_append_printf(buf, _(" (x%u)"), recurrenceGetMultiplier(first));
718 : }
719 0 : g_string_append_printf(buf, ": ");
720 0 : _monthly_append_when(first, buf);
721 0 : g_string_append_printf(buf, ", ");
722 0 : _monthly_append_when(second, buf);
723 : }
724 : else
725 : {
726 : /* Translators: %d is the number of Recurrences in the list. */
727 0 : g_string_printf(buf, _("Unknown, %d-size list."), rs_len);
728 : }
729 : }
730 : else
731 : {
732 0 : Recurrence *r = (Recurrence*)g_list_nth_data(rs, 0);
733 0 : guint multiplier = recurrenceGetMultiplier(r);
734 :
735 0 : switch (recurrenceGetPeriodType(r))
736 : {
737 0 : case PERIOD_ONCE:
738 : {
739 0 : g_string_printf(buf, "%s", _("Once"));
740 : }
741 0 : break;
742 0 : case PERIOD_DAY:
743 : {
744 0 : g_string_printf(buf, "%s", _("Daily"));
745 0 : if (multiplier > 1)
746 : {
747 : /* Translators: %u is the recurrence multiplier. */
748 0 : g_string_append_printf(buf, _(" (x%u)"), multiplier);
749 : }
750 : }
751 0 : break;
752 0 : case PERIOD_WEEK:
753 : {
754 0 : _weekly_list_to_compact_string(rs, buf);
755 : }
756 0 : break;
757 0 : case PERIOD_MONTH:
758 : case PERIOD_END_OF_MONTH:
759 : case PERIOD_LAST_WEEKDAY:
760 : {
761 0 : g_string_printf(buf, "%s", _("Monthly"));
762 0 : if (multiplier > 1)
763 : {
764 : /* Translators: %u is the recurrence multiplier. */
765 0 : g_string_append_printf(buf, _(" (x%u)"), multiplier);
766 : }
767 0 : g_string_append_printf(buf, ": ");
768 0 : _monthly_append_when(r, buf);
769 : }
770 0 : break;
771 0 : case PERIOD_NTH_WEEKDAY:
772 : {
773 : //g_warning("nth weekday not handled");
774 : //g_string_printf(buf, "@fixme: nth weekday not handled");
775 0 : g_string_printf(buf, "%s", _("Monthly"));
776 0 : if (multiplier > 1)
777 : {
778 : /* Translators: %u is the recurrence multiplier. */
779 0 : g_string_append_printf(buf, _(" (x%u)"), multiplier);
780 : }
781 0 : g_string_append_printf(buf, ": ");
782 0 : _monthly_append_when(r, buf);
783 : }
784 0 : break;
785 0 : case PERIOD_YEAR:
786 : {
787 0 : g_string_printf(buf, "%s", _("Yearly"));
788 0 : if (multiplier > 1)
789 : {
790 : /* Translators: %u is the recurrence multiplier. */
791 0 : g_string_append_printf(buf, _(" (x%u)"), multiplier);
792 : }
793 : }
794 0 : break;
795 0 : default:
796 0 : g_error("unknown Recurrence period %d", recurrenceGetPeriodType(r));
797 : break;
798 : }
799 : }
800 :
801 0 : rtn:
802 0 : return g_string_free(buf, FALSE);
803 : }
804 :
805 : /**
806 : * The ordering, in increasing degrees of frequent-ness:
807 : *
808 : * day < week < {nth-weekday < month < end-month, last_weekday} < year < once
809 : *
810 : * all the monthly types are basically together, but are broken down
811 : * internally cause they have to be ordered somehow.
812 : **/
813 : static int cmp_order_indexes[] =
814 : {
815 : 6, // PERIOD_ONCE
816 : 1, // PERIOD_DAY
817 : 2, // PERIOD_WEEK
818 : // 3, // "semi-monthly" ... Note that this isn't presently used, just the
819 : // // way the code worked out. :(
820 : 4, // PERIOD_MONTH
821 : 4, // PERIOD_END_OF_MONTH
822 : 4, // PERIOD_NTH_WEEKDAY
823 : 4, // PERIOD_LAST_WEEKDAY
824 : 5, // PERIOD_YEAR
825 : };
826 :
827 : static int cmp_monthly_order_indexes[] =
828 : {
829 : -1, // PERIOD_ONCE
830 : -1, // PERIOD_DAY
831 : -1, // PERIOD_WEEK
832 : 2, // PERIOD_MONTH
833 : 3, // PERIOD_END_OF_MONTH
834 : 1, // PERIOD_NTH_WEEKDAY
835 : 4, // PERIOD_LAST_WEEKDAY
836 : -1, // PERIOD_YEAR
837 : };
838 :
839 : int
840 0 : recurrenceCmp(Recurrence *a, Recurrence *b)
841 : {
842 : PeriodType period_a, period_b;
843 : int a_order_index, b_order_index;
844 : int a_mult, b_mult;
845 :
846 0 : g_return_val_if_fail(a != NULL && b != NULL, 0);
847 0 : g_return_val_if_fail(a != NULL, 1);
848 0 : g_return_val_if_fail(b != NULL, -1);
849 :
850 0 : period_a = recurrenceGetPeriodType(a);
851 0 : period_b = recurrenceGetPeriodType(b);
852 :
853 0 : a_order_index = cmp_order_indexes[period_a];
854 0 : b_order_index = cmp_order_indexes[period_b];
855 0 : if (a_order_index != b_order_index)
856 : {
857 0 : return a_order_index - b_order_index;
858 : }
859 0 : else if (a_order_index == cmp_order_indexes[PERIOD_MONTH])
860 : {
861 : // re-order intra-month options:
862 0 : a_order_index = cmp_monthly_order_indexes[period_a];
863 0 : b_order_index = cmp_monthly_order_indexes[period_b];
864 0 : g_assert(a_order_index != -1 && b_order_index != -1);
865 0 : if (a_order_index != b_order_index)
866 0 : return a_order_index - b_order_index;
867 : }
868 : /* else { the basic periods are equal; compare the multipliers } */
869 :
870 0 : a_mult = recurrenceGetMultiplier(a);
871 0 : b_mult = recurrenceGetMultiplier(b);
872 :
873 0 : return a_mult - b_mult;
874 : }
875 :
876 : int
877 0 : recurrenceListCmp(GList *a, GList *b)
878 : {
879 : Recurrence *most_freq_a, *most_freq_b;
880 :
881 0 : if (!a)
882 0 : return (b ? -1 : 0);
883 0 : else if (!b)
884 0 : return 1;
885 :
886 0 : most_freq_a = (Recurrence*)g_list_nth_data(g_list_sort(a, (GCompareFunc)recurrenceCmp), 0);
887 0 : most_freq_b = (Recurrence*)g_list_nth_data(g_list_sort(b, (GCompareFunc)recurrenceCmp), 0);
888 :
889 0 : return recurrenceCmp(most_freq_a, most_freq_b);
890 : }
891 :
892 : void
893 0 : recurrenceListFree(GList **recurrences)
894 : {
895 0 : g_list_free_full(*recurrences, (GDestroyNotify)g_free);
896 0 : *recurrences = NULL;
897 0 : }
|