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