GnuCash  5.6-150-g038405b370+
Recurrence.cpp
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 recurrenceGetPeriodType(const Recurrence *r)
59 {
60  return r ? r->ptype : PERIOD_INVALID;
61 }
62 
63 guint
64 recurrenceGetMultiplier(const Recurrence *r)
65 {
66  return r ? r->mult : 0;
67 }
68 
69 GDate
70 recurrenceGetDate(const Recurrence *r)
71 {
72  return r ? r->start : invalid_gdate;
73 }
74 
75 time64
76 recurrenceGetTime(const Recurrence *r)
77 {
78  return r ? gdate_to_time64(r->start) : INT64_MAX;
79 }
80 
81 WeekendAdjust
82 recurrenceGetWeekendAdjust(const Recurrence *r)
83 {
84  return r ? r->wadj : WEEKEND_ADJ_INVALID;
85 }
86 
87 void
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
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 
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
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
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
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
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 xaccAccountGetNoclosingBalanceChangeInCurrencyForPeriod (acc, t1, t2, TRUE);
430 }
431 
432 void
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  auto r = static_cast<const Recurrence *>(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 *
464 recurrenceToString(const Recurrence *r)
465 {
466  gchar *tmpDate, *ret;
467  const gchar *tmpPeriod;
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 *
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 const gchar *
522 recurrencePeriodTypeToString(PeriodType pt)
523 {
524  return VALID_PERIOD_TYPE(pt) ? period_type_strings[pt] : NULL;
525 }
526 
527 PeriodType
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 static_cast<PeriodType>(i);
535  return static_cast<PeriodType>(-1);
536 }
537 
538 const gchar *
539 recurrenceWeekendAdjustToString(WeekendAdjust wadj)
540 {
541  return VALID_WEEKEND_ADJ(wadj) ? weekend_adj_strings[wadj] : NULL;
542 }
543 
544 WeekendAdjust
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 static_cast<WeekendAdjust>(i);
552  return static_cast<WeekendAdjust>(-1);
553 }
554 
555 gboolean
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
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
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
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*
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 
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 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
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
893 recurrenceListFree(GList **recurrences)
894 {
895  g_list_foreach(*recurrences, (GFunc)g_free, NULL);
896  g_list_free(*recurrences);
897  *recurrences = NULL;
898 }
Date and Time handling routines.
STRUCTS.
time64 gnc_dmy2time64(gint day, gint month, gint year)
Convert a day, month, and year to a time64, returning the first second of the day.
void gnc_gdate_set_today(GDate *gd)
Set a GDate to the current day.
Definition: gnc-date.cpp:1236
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
Account handling public routines.
void gnc_dow_abbrev(gchar *buf, int buf_len, int dow)
Localized DOW abbreviation.
Definition: gnc-date.cpp:1365
time64 gdate_to_time64(GDate d)
Turns a GDate into a time64, returning the first second of the day.
Definition: gnc-date.cpp:1253
#define MAX_DATE_LENGTH
The maximum length of a string created by the date printers.
Definition: gnc-date.h:108
All type declarations for the whole Gnucash engine.
GLib helper routines.
time64 gnc_dmy2time64_end(gint day, gint month, gint year)
Same as gnc_dmy2time64, but last second of the day.
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition: gnc-date.h:87
gint gnc_list_length_cmp(const GList *list, size_t len)
Scans the GList elements the minimum number of iterations required to test it against a specified siz...