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