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 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 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
recurrenceGetPeriodType(const Recurrence * r)58 recurrenceGetPeriodType(const Recurrence *r)
59 {
60 return r ? r->ptype : PERIOD_INVALID;
61 }
62
63 guint
recurrenceGetMultiplier(const Recurrence * r)64 recurrenceGetMultiplier(const Recurrence *r)
65 {
66 return r ? r->mult : 0;
67 }
68
69 GDate
recurrenceGetDate(const Recurrence * r)70 recurrenceGetDate(const Recurrence *r)
71 {
72 return r ? r->start : invalid_gdate;
73 }
74
75 time64
recurrenceGetTime(const Recurrence * r)76 recurrenceGetTime(const Recurrence *r)
77 {
78 return r ? gdate_to_time64(r->start) : INT64_MAX;
79 }
80
81 WeekendAdjust
recurrenceGetWeekendAdjust(const Recurrence * r)82 recurrenceGetWeekendAdjust(const Recurrence *r)
83 {
84 return r ? r->wadj : WEEKEND_ADJ_INVALID;
85 }
86
87 void
recurrenceSet(Recurrence * r,guint16 mult,PeriodType pt,const GDate * _start,WeekendAdjust wadj)88 recurrenceSet(Recurrence *r, guint16 mult, PeriodType pt, const GDate *_start, WeekendAdjust wadj)
89 {
90 r->ptype = VALID_PERIOD_TYPE(pt) ? pt : PERIOD_MONTH;
91 r->mult = (pt == PERIOD_ONCE) ? 0 : (mult > 0 ? mult : 1);
92
93 if (_start && g_date_valid(_start))
94 {
95 r->start = *_start;
96 }
97 else
98 {
99 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 switch (r->ptype)
105 {
106 case PERIOD_END_OF_MONTH:
107 g_date_set_day(&r->start, g_date_get_days_in_month
108 (g_date_get_month(&r->start),
109 g_date_get_year(&r->start)));
110 break;
111 case PERIOD_LAST_WEEKDAY:
112 {
113 GDateDay dim;
114 dim = g_date_get_days_in_month(g_date_get_month(&r->start),
115 g_date_get_year(&r->start));
116 while (dim - g_date_get_day(&r->start) >= 7)
117 g_date_add_days(&r->start, 7);
118 }
119 break;
120 case PERIOD_NTH_WEEKDAY:
121 if ((g_date_get_day(&r->start) - 1) / 7 == 4) /* Fifth week */
122 r->ptype = PERIOD_LAST_WEEKDAY;
123 break;
124 default:
125 break;
126 }
127
128 switch (r->ptype)
129 {
130 case PERIOD_MONTH:
131 case PERIOD_END_OF_MONTH:
132 case PERIOD_YEAR:
133 r->wadj = wadj;
134 break;
135 default:
136 r->wadj = WEEKEND_ADJ_NONE;
137 break;
138 }
139 }
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
nth_weekday_compare(const GDate * start,const GDate * next,PeriodType pt)149 nth_weekday_compare(const GDate *start, const GDate *next, PeriodType pt)
150 {
151 GDateDay sd, nd;
152 gint matchday, dim, week;
153
154 nd = g_date_get_day(next);
155 sd = g_date_get_day(start);
156 week = sd / 7 > 3 ? 3 : sd / 7;
157 if (week > 0 && sd % 7 == 0 && sd != 28)
158 --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 matchday = 7 * week + //((sd - 1) / 7 == 4 ? 3 : (sd - 1) / 7) +
162 (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 dim = g_date_get_days_in_month(
166 g_date_get_month(next), g_date_get_year(next));
167 if ((dim - matchday) >= 7 && pt == PERIOD_LAST_WEEKDAY)
168 matchday += 7; /* Go to the fifth week, if needed */
169 if (pt == PERIOD_NTH_WEEKDAY && (matchday % 7 == 0))
170 matchday += 7;
171
172 return matchday - nd; /* Offset from 'next' to matchday */
173 }
174
175
adjust_for_weekend(PeriodType pt,WeekendAdjust wadj,GDate * date)176 static void adjust_for_weekend(PeriodType pt, WeekendAdjust wadj, GDate *date)
177 {
178 if (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH)
179 {
180 if (g_date_get_weekday(date) == G_DATE_SATURDAY || g_date_get_weekday(date) == G_DATE_SUNDAY)
181 {
182 switch (wadj)
183 {
184 case WEEKEND_ADJ_BACK:
185 g_date_subtract_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 1 : 2);
186 break;
187 case WEEKEND_ADJ_FORWARD:
188 g_date_add_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 2 : 1);
189 break;
190 case WEEKEND_ADJ_NONE:
191 default:
192 break;
193 }
194 }
195 }
196 }
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
recurrenceNextInstance(const Recurrence * r,const GDate * ref,GDate * next)203 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 g_return_if_fail(r);
212 g_return_if_fail(ref);
213 g_return_if_fail(g_date_valid(&r->start));
214 g_return_if_fail(g_date_valid(ref));
215
216 start = &r->start;
217 mult = r->mult;
218 pt = r->ptype;
219 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 adjusted_start = *start;
224 adjust_for_weekend(pt,wadj,&adjusted_start);
225 if (g_date_compare(ref, &adjusted_start) < 0)
226 {
227 g_date_set_julian(next, g_date_get_julian(&adjusted_start));
228 return;
229 }
230 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 switch (pt)
234 {
235 case PERIOD_YEAR:
236 mult *= 12;
237 /* fall through */
238 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 if (r->wadj == WEEKEND_ADJ_BACK &&
244 (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH) &&
245 (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 g_date_subtract_days(next, g_date_get_weekday(next) == G_DATE_SATURDAY ? 1 : 2);
250 }
251 if (r->wadj == WEEKEND_ADJ_BACK &&
252 (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH) &&
253 g_date_get_weekday(next) == G_DATE_FRIDAY)
254 {
255 GDate tmp_sat;
256 GDate tmp_sun;
257 g_date_set_julian(&tmp_sat, g_date_get_julian(next));
258 g_date_set_julian(&tmp_sun, g_date_get_julian(next));
259 g_date_add_days(&tmp_sat, 1);
260 g_date_add_days(&tmp_sun, 2);
261
262 if (pt == PERIOD_END_OF_MONTH)
263 {
264 if (g_date_is_last_of_month(next) ||
265 g_date_is_last_of_month(&tmp_sat) ||
266 g_date_is_last_of_month(&tmp_sun))
267 g_date_add_months(next, mult);
268 else
269 /* one fewer month fwd because of the occurrence in this month */
270 g_date_add_months(next, mult - 1);
271 }
272 else
273 {
274 if (g_date_get_day(&tmp_sat) == g_date_get_day(start))
275 {
276 g_date_add_days(next, 1);
277 g_date_add_months(next, mult);
278 }
279 else if (g_date_get_day(&tmp_sun) == g_date_get_day(start))
280 {
281 g_date_add_days(next, 2);
282 g_date_add_months(next, mult);
283 }
284 else if (g_date_get_day(next) >= g_date_get_day(start))
285 {
286 g_date_add_months(next, mult);
287 }
288 else if (g_date_is_last_of_month(next))
289 {
290 g_date_add_months(next, mult);
291 }
292 else if (g_date_is_last_of_month(&tmp_sat))
293 {
294 g_date_add_days(next, 1);
295 g_date_add_months(next, mult);
296 }
297 else if (g_date_is_last_of_month(&tmp_sun))
298 {
299 g_date_add_days(next, 2);
300 g_date_add_months(next, mult);
301 }
302 else
303 {
304 /* one fewer month fwd because of the occurrence in this month */
305 g_date_add_months(next, mult - 1);
306 }
307 }
308 }
309 else if ( g_date_is_last_of_month(next) ||
310 ((pt == PERIOD_MONTH || pt == PERIOD_YEAR) &&
311 g_date_get_day(next) >= g_date_get_day(start)) ||
312 ((pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY) &&
313 nth_weekday_compare(start, next, pt) <= 0) )
314 g_date_add_months(next, mult);
315 else
316 /* one fewer month fwd because of the occurrence in this month */
317 g_date_add_months(next, mult - 1);
318 break;
319 case PERIOD_WEEK:
320 mult *= 7;
321 /* fall through */
322 case PERIOD_DAY:
323 g_date_add_days(next, mult);
324 break;
325 case PERIOD_ONCE:
326 g_date_clear(next, 1); /* We already caught the case where ref is */
327 return; /* earlier than start, so this is invalid. */
328 default:
329 PERR("Invalid period type");
330 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 switch (pt)
336 {
337 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 n_months = 12 * (g_date_get_year(next) - g_date_get_year(start)) +
346 (g_date_get_month(next) - g_date_get_month(start));
347 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 dim = g_date_get_days_in_month(g_date_get_month(next),
352 g_date_get_year(next));
353 if (pt == PERIOD_LAST_WEEKDAY || pt == PERIOD_NTH_WEEKDAY)
354 {
355 gint wdresult = nth_weekday_compare(start, next, pt);
356 if (wdresult < 0)
357 {
358 wdresult = -wdresult;
359 g_date_subtract_days(next, wdresult);
360 }
361 else
362 g_date_add_days(next, wdresult);
363 }
364 else if (pt == PERIOD_END_OF_MONTH || g_date_get_day(start) >= dim)
365 g_date_set_day(next, dim); /* last day in the month */
366 else
367 g_date_set_day(next, g_date_get_day(start)); /*same day as start*/
368
369 /* Adjust for dates on the weekend. */
370 adjust_for_weekend(pt,wadj,next);
371 }
372 break;
373 case PERIOD_WEEK:
374 case PERIOD_DAY:
375 g_date_subtract_days(next, g_date_days_between(start, next) % mult);
376 break;
377 default:
378 PERR("Invalid period type");
379 break;
380 }
381 }
382
383 /* Zero-based index */
384 void
recurrenceNthInstance(const Recurrence * r,guint n,GDate * date)385 recurrenceNthInstance(const Recurrence *r, guint n, GDate *date)
386 {
387 GDate ref;
388 guint i;
389
390 for (*date = ref = r->start, i = 0; i < n; i++)
391 {
392 recurrenceNextInstance(r, &ref, date);
393 ref = *date;
394 }
395 }
396
397 time64
recurrenceGetPeriodTime(const Recurrence * r,guint period_num,gboolean end)398 recurrenceGetPeriodTime(const Recurrence *r, guint period_num, gboolean end)
399 {
400 GDate date;
401 time64 time;
402 recurrenceNthInstance(r, period_num + (end ? 1 : 0), &date);
403 if (end)
404 {
405 g_date_subtract_days(&date, 1);
406 time = gnc_dmy2time64_end (g_date_get_day(&date),
407 g_date_get_month(&date),
408 g_date_get_year (&date));
409
410 }
411 else
412 {
413 time = gnc_dmy2time64 (g_date_get_day(&date),
414 g_date_get_month(&date),
415 g_date_get_year (&date));
416 }
417 return time;
418 }
419
420 gnc_numeric
recurrenceGetAccountPeriodValue(const Recurrence * r,Account * acc,guint n)421 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 g_return_val_if_fail(r && acc, gnc_numeric_zero());
427 t1 = recurrenceGetPeriodTime(r, n, FALSE);
428 t2 = recurrenceGetPeriodTime(r, n, TRUE);
429 return xaccAccountGetNoclosingBalanceChangeForPeriod (acc, t1, t2, TRUE);
430 }
431
432 void
recurrenceListNextInstance(const GList * rlist,const GDate * ref,GDate * next)433 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 g_date_clear(next, 1);
439
440 // empty rlist = no recurrence
441 if (rlist == NULL)
442 {
443 return;
444 }
445
446 g_return_if_fail(ref && next && g_date_valid(ref));
447
448 for (iter = rlist; iter; iter = iter->next)
449 {
450 const Recurrence *r = iter->data;
451
452 recurrenceNextInstance(r, ref, &nextSingle);
453 if (!g_date_valid(&nextSingle)) continue;
454
455 if (g_date_valid(next))
456 g_date_order(next, &nextSingle); /* swaps dates if needed */
457 else
458 *next = nextSingle; /* first date is always earliest so far */
459 }
460 }
461
462 /* Caller owns the returned memory */
463 gchar *
recurrenceToString(const Recurrence * r)464 recurrenceToString(const Recurrence *r)
465 {
466 gchar *tmpDate;
467 gchar *tmpPeriod, *ret;
468
469 g_return_val_if_fail(g_date_valid(&r->start), NULL);
470 tmpDate = g_new0(gchar, MAX_DATE_LENGTH + 1);
471 g_date_strftime(tmpDate, MAX_DATE_LENGTH, "%x", &r->start);
472
473 if (r->ptype == PERIOD_ONCE)
474 {
475 ret = g_strdup_printf("once on %s", tmpDate);
476 goto done;
477 }
478
479 tmpPeriod = period_type_strings[r->ptype];
480 if (r->mult > 1)
481 ret = g_strdup_printf("Every %d %ss beginning %s",
482 r->mult, tmpPeriod, tmpDate);
483 else
484 ret = g_strdup_printf("Every %s beginning %s",
485 tmpPeriod, tmpDate);
486 done:
487 g_free(tmpDate);
488 return ret;
489 }
490
491 /* caller owns the returned memory */
492 gchar *
recurrenceListToString(const GList * r)493 recurrenceListToString(const GList *r)
494 {
495 const GList *iter;
496 GString *str;
497 gchar *s;
498
499 str = g_string_new("");
500 if (r == NULL)
501 {
502 g_string_append(str, _("None"));
503 }
504 else
505 {
506 for (iter = r; iter; iter = iter->next)
507 {
508 if (iter != r)
509 {
510 /* Translators: " + " is an separator in a list of string-representations of recurrence frequencies */
511 g_string_append(str, _(" + "));
512 }
513 s = recurrenceToString((Recurrence *)iter->data);
514 g_string_append(str, s);
515 g_free(s);
516 }
517 }
518 return g_string_free(str, FALSE);
519 }
520
521 gchar *
recurrencePeriodTypeToString(PeriodType pt)522 recurrencePeriodTypeToString(PeriodType pt)
523 {
524 return VALID_PERIOD_TYPE(pt) ? g_strdup(period_type_strings[pt]) : NULL;
525 }
526
527 PeriodType
recurrencePeriodTypeFromString(const gchar * str)528 recurrencePeriodTypeFromString(const gchar *str)
529 {
530 int i;
531
532 for (i = 0; i < NUM_PERIOD_TYPES; i++)
533 if (g_strcmp0(period_type_strings[i], str) == 0)
534 return i;
535 return -1;
536 }
537
538 gchar *
recurrenceWeekendAdjustToString(WeekendAdjust wadj)539 recurrenceWeekendAdjustToString(WeekendAdjust wadj)
540 {
541 return VALID_WEEKEND_ADJ(wadj) ? g_strdup(weekend_adj_strings[wadj]) : NULL;
542 }
543
544 WeekendAdjust
recurrenceWeekendAdjustFromString(const gchar * str)545 recurrenceWeekendAdjustFromString(const gchar *str)
546 {
547 int i;
548
549 for (i = 0; i < NUM_WEEKEND_ADJS; i++)
550 if (g_strcmp0(weekend_adj_strings[i], str) == 0)
551 return i;
552 return -1;
553 }
554
555 gboolean
recurrenceListIsSemiMonthly(GList * recurrences)556 recurrenceListIsSemiMonthly(GList *recurrences)
557 {
558 if (gnc_list_length_cmp (recurrences, 2))
559 return FALSE;
560
561 // should be a "semi-monthly":
562 {
563 Recurrence *first = (Recurrence*)g_list_nth_data(recurrences, 0);
564 Recurrence *second = (Recurrence*)g_list_nth_data(recurrences, 1);
565 PeriodType first_period, second_period;
566 first_period = recurrenceGetPeriodType(first);
567 second_period = recurrenceGetPeriodType(second);
568
569 if (!((first_period == PERIOD_MONTH
570 || first_period == PERIOD_END_OF_MONTH
571 || first_period == PERIOD_LAST_WEEKDAY)
572 && (second_period == PERIOD_MONTH
573 || second_period == PERIOD_END_OF_MONTH
574 || 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 return FALSE;
579 }
580 }
581 return TRUE;
582 }
583
584 gboolean
recurrenceListIsWeeklyMultiple(const GList * recurrences)585 recurrenceListIsWeeklyMultiple(const GList *recurrences)
586 {
587 const GList *r_iter;
588
589 for (r_iter = recurrences; r_iter != NULL; r_iter = r_iter->next)
590 {
591 Recurrence *r = (Recurrence*)r_iter->data;
592 if (recurrenceGetPeriodType(r) != PERIOD_WEEK)
593 {
594 return FALSE;
595 }
596 }
597 return TRUE;
598 }
599
600 static void
_weekly_list_to_compact_string(GList * rs,GString * buf)601 _weekly_list_to_compact_string(GList *rs, GString *buf)
602 {
603 int dow_idx;
604 char dow_present_bits = 0;
605 int multiplier = -1;
606 for (; rs != NULL; rs = rs->next)
607 {
608 Recurrence *r = (Recurrence*)rs->data;
609 GDate date = recurrenceGetDate(r);
610 GDateWeekday dow = g_date_get_weekday(&date);
611 if (dow == G_DATE_BAD_WEEKDAY)
612 {
613 g_critical("bad weekday pretty-printing recurrence");
614 continue;
615 }
616 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 multiplier = recurrenceGetMultiplier(r);
621 }
622 g_string_printf(buf, "%s", _("Weekly"));
623 if (multiplier > 1)
624 {
625 /* Translators: %u is the recurrence multiplier, i.e. this
626 event should occur every %u'th week. */
627 g_string_append_printf(buf, _(" (x%u)"), multiplier);
628 }
629 g_string_append_printf(buf, ": ");
630
631 // @@fixme: this is only Sunday-started weeks. :/
632 for (dow_idx = 0; dow_idx < 7; dow_idx++)
633 {
634 if ((dow_present_bits & (1 << dow_idx)) != 0)
635 {
636 gchar dbuf[10];
637 gnc_dow_abbrev(dbuf, 10, dow_idx);
638 g_string_append_unichar(buf, g_utf8_get_char(dbuf));
639 }
640 else
641 {
642 g_string_append_printf(buf, "-");
643 }
644 }
645 }
646
647 /* A constant is needed for the array size */
648 #define abbrev_day_name_bufsize 10
649 static void
_monthly_append_when(Recurrence * r,GString * buf)650 _monthly_append_when(Recurrence *r, GString *buf)
651 {
652 GDate date = recurrenceGetDate(r);
653 if (recurrenceGetPeriodType(r) == PERIOD_LAST_WEEKDAY)
654 {
655 gchar day_name_buf[abbrev_day_name_bufsize];
656
657 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 g_string_append_printf(buf, _("last %s"), day_name_buf);
661 }
662 else if (recurrenceGetPeriodType(r) == PERIOD_NTH_WEEKDAY)
663 {
664 int week = 0;
665 int day_of_month_index = 0;
666 const char *numerals[] = {N_("1st"), N_("2nd"), N_("3rd"), N_("4th")};
667 gchar day_name_buf[abbrev_day_name_bufsize];
668
669 gnc_dow_abbrev(day_name_buf, abbrev_day_name_bufsize, g_date_get_weekday(&date) % 7);
670 day_of_month_index = g_date_get_day(&date) - 1;
671 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 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 g_string_append_printf(buf, "%u", g_date_get_day(&date));
680 }
681 }
682
683 gchar*
recurrenceListToCompactString(GList * rs)684 recurrenceListToCompactString(GList *rs)
685 {
686 GString *buf = g_string_sized_new(16);
687 gint rs_len = g_list_length (rs);
688
689 if (rs_len == 0)
690 {
691 g_string_printf(buf, "%s", _("None"));
692 goto rtn;
693 }
694
695 if (rs_len > 1)
696 {
697 if (recurrenceListIsWeeklyMultiple(rs))
698 {
699 _weekly_list_to_compact_string(rs, buf);
700 }
701 else if (recurrenceListIsSemiMonthly(rs))
702 {
703 Recurrence *first, *second;
704 first = (Recurrence*)g_list_nth_data(rs, 0);
705 second = (Recurrence*)g_list_nth_data(rs, 1);
706 if (recurrenceGetMultiplier(first) != recurrenceGetMultiplier(second))
707 {
708 g_warning("lying about non-equal semi-monthly recurrence multiplier: %d vs. %d",
709 recurrenceGetMultiplier(first), recurrenceGetMultiplier(second));
710 }
711
712 g_string_printf(buf, "%s", _("Semi-monthly"));
713 g_string_append_printf(buf, " ");
714 if (recurrenceGetMultiplier(first) > 1)
715 {
716 /* Translators: %u is the recurrence multiplier number */
717 g_string_append_printf(buf, _(" (x%u)"), recurrenceGetMultiplier(first));
718 }
719 g_string_append_printf(buf, ": ");
720 _monthly_append_when(first, buf);
721 g_string_append_printf(buf, ", ");
722 _monthly_append_when(second, buf);
723 }
724 else
725 {
726 /* Translators: %d is the number of Recurrences in the list. */
727 g_string_printf(buf, _("Unknown, %d-size list."), rs_len);
728 }
729 }
730 else
731 {
732 Recurrence *r = (Recurrence*)g_list_nth_data(rs, 0);
733 guint multiplier = recurrenceGetMultiplier(r);
734
735 switch (recurrenceGetPeriodType(r))
736 {
737 case PERIOD_ONCE:
738 {
739 g_string_printf(buf, "%s", _("Once"));
740 }
741 break;
742 case PERIOD_DAY:
743 {
744 g_string_printf(buf, "%s", _("Daily"));
745 if (multiplier > 1)
746 {
747 /* Translators: %u is the recurrence multiplier. */
748 g_string_append_printf(buf, _(" (x%u)"), multiplier);
749 }
750 }
751 break;
752 case PERIOD_WEEK:
753 {
754 _weekly_list_to_compact_string(rs, buf);
755 }
756 break;
757 case PERIOD_MONTH:
758 case PERIOD_END_OF_MONTH:
759 case PERIOD_LAST_WEEKDAY:
760 {
761 g_string_printf(buf, "%s", _("Monthly"));
762 if (multiplier > 1)
763 {
764 /* Translators: %u is the recurrence multiplier. */
765 g_string_append_printf(buf, _(" (x%u)"), multiplier);
766 }
767 g_string_append_printf(buf, ": ");
768 _monthly_append_when(r, buf);
769 }
770 break;
771 case PERIOD_NTH_WEEKDAY:
772 {
773 //g_warning("nth weekday not handled");
774 //g_string_printf(buf, "@fixme: nth weekday not handled");
775 g_string_printf(buf, "%s", _("Monthly"));
776 if (multiplier > 1)
777 {
778 /* Translators: %u is the recurrence multiplier. */
779 g_string_append_printf(buf, _(" (x%u)"), multiplier);
780 }
781 g_string_append_printf(buf, ": ");
782 _monthly_append_when(r, buf);
783 }
784 break;
785 case PERIOD_YEAR:
786 {
787 g_string_printf(buf, "%s", _("Yearly"));
788 if (multiplier > 1)
789 {
790 /* Translators: %u is the recurrence multiplier. */
791 g_string_append_printf(buf, _(" (x%u)"), multiplier);
792 }
793 }
794 break;
795 default:
796 g_error("unknown Recurrence period %d", recurrenceGetPeriodType(r));
797 break;
798 }
799 }
800
801 rtn:
802 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
recurrenceCmp(Recurrence * a,Recurrence * b)840 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 g_return_val_if_fail(a != NULL && b != NULL, 0);
847 g_return_val_if_fail(a != NULL, 1);
848 g_return_val_if_fail(b != NULL, -1);
849
850 period_a = recurrenceGetPeriodType(a);
851 period_b = recurrenceGetPeriodType(b);
852
853 a_order_index = cmp_order_indexes[period_a];
854 b_order_index = cmp_order_indexes[period_b];
855 if (a_order_index != b_order_index)
856 {
857 return a_order_index - b_order_index;
858 }
859 else if (a_order_index == cmp_order_indexes[PERIOD_MONTH])
860 {
861 // re-order intra-month options:
862 a_order_index = cmp_monthly_order_indexes[period_a];
863 b_order_index = cmp_monthly_order_indexes[period_b];
864 g_assert(a_order_index != -1 && b_order_index != -1);
865 if (a_order_index != b_order_index)
866 return a_order_index - b_order_index;
867 }
868 /* else { the basic periods are equal; compare the multipliers } */
869
870 a_mult = recurrenceGetMultiplier(a);
871 b_mult = recurrenceGetMultiplier(b);
872
873 return a_mult - b_mult;
874 }
875
876 int
recurrenceListCmp(GList * a,GList * b)877 recurrenceListCmp(GList *a, GList *b)
878 {
879 Recurrence *most_freq_a, *most_freq_b;
880
881 if (!a)
882 return (b ? -1 : 0);
883 else if (!b)
884 return 1;
885
886 most_freq_a = (Recurrence*)g_list_nth_data(g_list_sort(a, (GCompareFunc)recurrenceCmp), 0);
887 most_freq_b = (Recurrence*)g_list_nth_data(g_list_sort(b, (GCompareFunc)recurrenceCmp), 0);
888
889 return recurrenceCmp(most_freq_a, most_freq_b);
890 }
891
892 void
recurrenceListFree(GList ** recurrences)893 recurrenceListFree(GList **recurrences)
894 {
895 g_list_foreach(*recurrences, (GFunc)g_free, NULL);
896 g_list_free(*recurrences);
897 *recurrences = NULL;
898 }
899