Branch data 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 : 21635868 : static void adjust_for_weekend(PeriodType pt, WeekendAdjust wadj, GDate *date)
177 : : {
178 : 21635868 : if (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH)
179 : : {
180 : 8135771 : if (g_date_get_weekday(date) == G_DATE_SATURDAY || g_date_get_weekday(date) == G_DATE_SUNDAY)
181 : : {
182 : 2105038 : 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 : 719038 : case WEEKEND_ADJ_NONE:
191 : : default:
192 : 719038 : break;
193 : : }
194 : : }
195 : : }
196 : 21635868 : }
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 : 21617972 : 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 : 43218020 : g_return_if_fail(r);
212 : 21617972 : g_return_if_fail(ref);
213 : 21617972 : g_return_if_fail(g_date_valid(&r->start));
214 : 21617972 : g_return_if_fail(g_date_valid(ref));
215 : :
216 : 21617972 : start = &r->start;
217 : 21617972 : mult = r->mult;
218 : 21617972 : pt = r->ptype;
219 : 21617972 : 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 : 21617972 : adjusted_start = *start;
224 : 21617972 : adjust_for_weekend(pt,wadj,&adjusted_start);
225 : 21617972 : 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 : 17934 : 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 : 17934 : switch (pt)
234 : : {
235 : 6 : case PERIOD_YEAR:
236 : 6 : mult *= 12;
237 : : /* fall through */
238 : 17896 : 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 : 17896 : (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 : 17896 : (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 : 35785 : else if ( g_date_is_last_of_month(next) ||
310 : 13 : ((pt == PERIOD_MONTH || pt == PERIOD_YEAR) &&
311 : 35798 : 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 : 17888 : 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 : 17896 : 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 : 17924 : switch (pt)
336 : : {
337 : 17896 : 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 : 17896 : n_months = 12 * (g_date_get_year(next) - g_date_get_year(start)) +
346 : 17896 : (g_date_get_month(next) - g_date_get_month(start));
347 : 17896 : 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 : 17896 : dim = g_date_get_days_in_month(g_date_get_month(next),
352 : 17896 : g_date_get_year(next));
353 : 17896 : 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 : 17885 : 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 : 17882 : g_date_set_day(next, g_date_get_day(start)); /*same day as start*/
368 : :
369 : : /* Adjust for dates on the weekend. */
370 : 17896 : adjust_for_weekend(pt,wadj,next);
371 : : }
372 : 17896 : 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 : 7020 : recurrenceNthInstance(const Recurrence *r, guint n, GDate *date)
386 : : {
387 : : GDate ref;
388 : : guint i;
389 : :
390 : 24891 : for (*date = ref = r->start, i = 0; i < n; i++)
391 : : {
392 : 17871 : recurrenceNextInstance(r, &ref, date);
393 : 17871 : ref = *date;
394 : : }
395 : 7020 : }
396 : :
397 : : time64
398 : 7020 : recurrenceGetPeriodTime(const Recurrence *r, guint period_num, gboolean end)
399 : : {
400 : : GDate date;
401 : : time64 time;
402 : 7020 : recurrenceNthInstance(r, period_num + (end ? 1 : 0), &date);
403 : 7020 : if (end)
404 : : {
405 : 2823 : g_date_subtract_days(&date, 1);
406 : 2823 : time = gnc_dmy2time64_end (g_date_get_day(&date),
407 : 2823 : g_date_get_month(&date),
408 : 2823 : g_date_get_year (&date));
409 : :
410 : : }
411 : : else
412 : : {
413 : 4197 : time = gnc_dmy2time64 (g_date_get_day(&date),
414 : 4197 : g_date_get_month(&date),
415 : 4197 : g_date_get_year (&date));
416 : : }
417 : 7020 : return time;
418 : : }
419 : :
420 : : gnc_numeric
421 : 2119 : 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 : 2119 : g_return_val_if_fail(r && acc, gnc_numeric_zero());
427 : 2119 : t1 = recurrenceGetPeriodTime(r, n, FALSE);
428 : 2119 : t2 = recurrenceGetPeriodTime(r, n, TRUE);
429 : 2119 : 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_foreach(*recurrences, (GFunc)g_free, NULL);
896 : 0 : g_list_free(*recurrences);
897 : 0 : *recurrences = NULL;
898 : 0 : }
|