24 #include <glib/gi18n.h> 27 #include "Recurrence.h" 36 #define LOG_MOD "gnc.engine.recurrence" 37 static QofLogModule log_module = LOG_MOD;
39 #define G_LOG_DOMAIN LOG_MOD 41 static GDate invalid_gdate;
44 static const gchar *period_type_strings[NUM_PERIOD_TYPES] =
46 "once",
"day",
"week",
"month",
"end of month",
47 "nth weekday",
"last weekday",
"year",
49 static const gchar *weekend_adj_strings[NUM_WEEKEND_ADJS] =
51 "none",
"back",
"forward",
54 #define VALID_PERIOD_TYPE(pt) ((0 <= (pt)) && ((pt) < NUM_PERIOD_TYPES)) 55 #define VALID_WEEKEND_ADJ(wadj) ((0 <= (wadj)) && ((wadj) < NUM_WEEKEND_ADJS)) 60 return r ? r->ptype : PERIOD_INVALID;
66 return r ? r->mult : 0;
72 return r ? r->start : invalid_gdate;
82 recurrenceGetWeekendAdjust(
const Recurrence *r)
84 return r ? r->wadj : WEEKEND_ADJ_INVALID;
88 recurrenceSet(
Recurrence *r, guint16 mult, PeriodType pt,
const GDate *_start, WeekendAdjust wadj)
90 r->ptype = VALID_PERIOD_TYPE(pt) ? pt : PERIOD_MONTH;
91 r->mult = (pt == PERIOD_ONCE) ? 0 : (mult > 0 ? mult : 1);
93 if (_start && g_date_valid(_start))
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)));
111 case PERIOD_LAST_WEEKDAY:
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);
120 case PERIOD_NTH_WEEKDAY:
121 if ((g_date_get_day(&r->start) - 1) / 7 == 4)
122 r->ptype = PERIOD_LAST_WEEKDAY;
131 case PERIOD_END_OF_MONTH:
136 r->wadj = WEEKEND_ADJ_NONE;
149 nth_weekday_compare(
const GDate *start,
const GDate *next, PeriodType pt)
152 gint matchday, dim, week;
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)
161 matchday = 7 * week +
162 (nd - g_date_get_weekday(next) + g_date_get_weekday(start) + 7) % 7;
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)
169 if (pt == PERIOD_NTH_WEEKDAY && (matchday % 7 == 0))
172 return matchday - nd;
176 static void adjust_for_weekend(PeriodType pt, WeekendAdjust wadj, GDate *date)
178 if (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH)
180 if (g_date_get_weekday(date) == G_DATE_SATURDAY || g_date_get_weekday(date) == G_DATE_SUNDAY)
184 case WEEKEND_ADJ_BACK:
185 g_date_subtract_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 1 : 2);
187 case WEEKEND_ADJ_FORWARD:
188 g_date_add_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 2 : 1);
190 case WEEKEND_ADJ_NONE:
203 recurrenceNextInstance(
const Recurrence *r,
const GDate *ref, GDate *next)
207 GDate adjusted_start;
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));
223 adjusted_start = *start;
224 adjust_for_weekend(pt,wadj,&adjusted_start);
225 if (g_date_compare(ref, &adjusted_start) < 0)
227 g_date_set_julian(next, g_date_get_julian(&adjusted_start));
230 g_date_set_julian(next, g_date_get_julian(ref));
239 case PERIOD_NTH_WEEKDAY:
240 case PERIOD_LAST_WEEKDAY:
241 case PERIOD_END_OF_MONTH:
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))
249 g_date_subtract_days(next, g_date_get_weekday(next) == G_DATE_SATURDAY ? 1 : 2);
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)
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);
262 if (pt == PERIOD_END_OF_MONTH)
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);
270 g_date_add_months(next, mult - 1);
274 if (g_date_get_day(&tmp_sat) == g_date_get_day(start))
276 g_date_add_days(next, 1);
277 g_date_add_months(next, mult);
279 else if (g_date_get_day(&tmp_sun) == g_date_get_day(start))
281 g_date_add_days(next, 2);
282 g_date_add_months(next, mult);
284 else if (g_date_get_day(next) >= g_date_get_day(start))
286 g_date_add_months(next, mult);
288 else if (g_date_is_last_of_month(next))
290 g_date_add_months(next, mult);
292 else if (g_date_is_last_of_month(&tmp_sat))
294 g_date_add_days(next, 1);
295 g_date_add_months(next, mult);
297 else if (g_date_is_last_of_month(&tmp_sun))
299 g_date_add_days(next, 2);
300 g_date_add_months(next, mult);
305 g_date_add_months(next, mult - 1);
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);
317 g_date_add_months(next, mult - 1);
323 g_date_add_days(next, mult);
326 g_date_clear(next, 1);
329 PERR(
"Invalid period type");
339 case PERIOD_NTH_WEEKDAY:
340 case PERIOD_LAST_WEEKDAY:
341 case PERIOD_END_OF_MONTH:
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);
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)
355 gint wdresult = nth_weekday_compare(start, next, pt);
358 wdresult = -wdresult;
359 g_date_subtract_days(next, wdresult);
362 g_date_add_days(next, wdresult);
364 else if (pt == PERIOD_END_OF_MONTH || g_date_get_day(start) >= dim)
365 g_date_set_day(next, dim);
367 g_date_set_day(next, g_date_get_day(start));
370 adjust_for_weekend(pt,wadj,next);
375 g_date_subtract_days(next, g_date_days_between(start, next) % mult);
378 PERR(
"Invalid period type");
385 recurrenceNthInstance(
const Recurrence *r, guint n, GDate *date)
390 for (*date = ref = r->start, i = 0; i < n; i++)
392 recurrenceNextInstance(r, &ref, date);
398 recurrenceGetPeriodTime(
const Recurrence *r, guint period_num, gboolean end)
402 recurrenceNthInstance(r, period_num + (end ? 1 : 0), &date);
405 g_date_subtract_days(&date, 1);
407 g_date_get_month(&date),
408 g_date_get_year (&date));
414 g_date_get_month(&date),
415 g_date_get_year (&date));
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);
433 recurrenceListNextInstance(
const GList *rlist,
const GDate *ref, GDate *next)
438 g_date_clear(next, 1);
446 g_return_if_fail(ref && next && g_date_valid(ref));
448 for (iter = rlist; iter; iter = iter->next)
450 auto r =
static_cast<const Recurrence *
>(iter->data);
452 recurrenceNextInstance(r, ref, &nextSingle);
453 if (!g_date_valid(&nextSingle))
continue;
455 if (g_date_valid(next))
456 g_date_order(next, &nextSingle);
466 gchar *tmpDate, *ret;
467 const gchar *tmpPeriod;
469 g_return_val_if_fail(g_date_valid(&r->start), NULL);
473 if (r->ptype == PERIOD_ONCE)
475 ret = g_strdup_printf(
"once on %s", tmpDate);
479 tmpPeriod = period_type_strings[r->ptype];
481 ret = g_strdup_printf(
"Every %d %ss beginning %s",
482 r->mult, tmpPeriod, tmpDate);
484 ret = g_strdup_printf(
"Every %s beginning %s",
493 recurrenceListToString(
const GList *r)
499 str = g_string_new(
"");
502 g_string_append(str, _(
"None"));
506 for (iter = r; iter; iter = iter->next)
511 g_string_append(str, _(
" + "));
513 s = recurrenceToString((
Recurrence *)iter->data);
514 g_string_append(str, s);
518 return g_string_free(str, FALSE);
522 recurrencePeriodTypeToString(PeriodType pt)
524 return VALID_PERIOD_TYPE(pt) ? period_type_strings[pt] : NULL;
528 recurrencePeriodTypeFromString(
const gchar *str)
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);
539 recurrenceWeekendAdjustToString(WeekendAdjust wadj)
541 return VALID_WEEKEND_ADJ(wadj) ? weekend_adj_strings[wadj] : NULL;
545 recurrenceWeekendAdjustFromString(
const gchar *str)
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);
556 recurrenceListIsSemiMonthly(GList *recurrences)
565 PeriodType first_period, second_period;
566 first_period = recurrenceGetPeriodType(first);
567 second_period = recurrenceGetPeriodType(second);
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)))
585 recurrenceListIsWeeklyMultiple(
const GList *recurrences)
589 for (r_iter = recurrences; r_iter != NULL; r_iter = r_iter->next)
592 if (recurrenceGetPeriodType(r) != PERIOD_WEEK)
601 _weekly_list_to_compact_string(GList *rs, GString *buf)
604 char dow_present_bits = 0;
606 for (; rs != NULL; rs = rs->next)
609 GDate date = recurrenceGetDate(r);
610 GDateWeekday dow = g_date_get_weekday(&date);
611 if (dow == G_DATE_BAD_WEEKDAY)
613 g_critical(
"bad weekday pretty-printing recurrence");
616 dow_present_bits |= (1 << (dow % 7));
620 multiplier = recurrenceGetMultiplier(r);
622 g_string_printf(buf,
"%s", _(
"Weekly"));
627 g_string_append_printf(buf, _(
" (x%u)"), multiplier);
629 g_string_append_printf(buf,
": ");
632 for (dow_idx = 0; dow_idx < 7; dow_idx++)
634 if ((dow_present_bits & (1 << dow_idx)) != 0)
638 g_string_append_unichar(buf, g_utf8_get_char(dbuf));
642 g_string_append_printf(buf,
"-");
648 #define abbrev_day_name_bufsize 10 650 _monthly_append_when(
Recurrence *r, GString *buf)
652 GDate date = recurrenceGetDate(r);
653 if (recurrenceGetPeriodType(r) == PERIOD_LAST_WEEKDAY)
655 gchar day_name_buf[abbrev_day_name_bufsize];
657 gnc_dow_abbrev(day_name_buf, abbrev_day_name_bufsize, g_date_get_weekday(&date) % 7);
660 g_string_append_printf(buf, _(
"last %s"), day_name_buf);
662 else if (recurrenceGetPeriodType(r) == PERIOD_NTH_WEEKDAY)
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];
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;
674 g_string_append_printf(buf, _(
"%s %s"), _(numerals[week]), day_name_buf);
679 g_string_append_printf(buf,
"%u", g_date_get_day(&date));
684 recurrenceListToCompactString(GList *rs)
686 GString *buf = g_string_sized_new(16);
687 gint rs_len = g_list_length (rs);
691 g_string_printf(buf,
"%s", _(
"None"));
697 if (recurrenceListIsWeeklyMultiple(rs))
699 _weekly_list_to_compact_string(rs, buf);
701 else if (recurrenceListIsSemiMonthly(rs))
706 if (recurrenceGetMultiplier(first) != recurrenceGetMultiplier(second))
708 g_warning(
"lying about non-equal semi-monthly recurrence multiplier: %d vs. %d",
709 recurrenceGetMultiplier(first), recurrenceGetMultiplier(second));
712 g_string_printf(buf,
"%s", _(
"Semi-monthly"));
713 g_string_append_printf(buf,
" ");
714 if (recurrenceGetMultiplier(first) > 1)
717 g_string_append_printf(buf, _(
" (x%u)"), recurrenceGetMultiplier(first));
719 g_string_append_printf(buf,
": ");
720 _monthly_append_when(first, buf);
721 g_string_append_printf(buf,
", ");
722 _monthly_append_when(second, buf);
727 g_string_printf(buf, _(
"Unknown, %d-size list."), rs_len);
733 guint multiplier = recurrenceGetMultiplier(r);
735 switch (recurrenceGetPeriodType(r))
739 g_string_printf(buf,
"%s", _(
"Once"));
744 g_string_printf(buf,
"%s", _(
"Daily"));
748 g_string_append_printf(buf, _(
" (x%u)"), multiplier);
754 _weekly_list_to_compact_string(rs, buf);
758 case PERIOD_END_OF_MONTH:
759 case PERIOD_LAST_WEEKDAY:
761 g_string_printf(buf,
"%s", _(
"Monthly"));
765 g_string_append_printf(buf, _(
" (x%u)"), multiplier);
767 g_string_append_printf(buf,
": ");
768 _monthly_append_when(r, buf);
771 case PERIOD_NTH_WEEKDAY:
775 g_string_printf(buf,
"%s", _(
"Monthly"));
779 g_string_append_printf(buf, _(
" (x%u)"), multiplier);
781 g_string_append_printf(buf,
": ");
782 _monthly_append_when(r, buf);
787 g_string_printf(buf,
"%s", _(
"Yearly"));
791 g_string_append_printf(buf, _(
" (x%u)"), multiplier);
796 g_error(
"unknown Recurrence period %d", recurrenceGetPeriodType(r));
802 return g_string_free(buf, FALSE);
813 static int cmp_order_indexes[] =
827 static int cmp_monthly_order_indexes[] =
842 PeriodType period_a, period_b;
843 int a_order_index, b_order_index;
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);
850 period_a = recurrenceGetPeriodType(a);
851 period_b = recurrenceGetPeriodType(b);
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)
857 return a_order_index - b_order_index;
859 else if (a_order_index == cmp_order_indexes[PERIOD_MONTH])
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;
870 a_mult = recurrenceGetMultiplier(a);
871 b_mult = recurrenceGetMultiplier(b);
873 return a_mult - b_mult;
877 recurrenceListCmp(GList *a, GList *b)
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);
889 return recurrenceCmp(most_freq_a, most_freq_b);
893 recurrenceListFree(GList **recurrences)
895 g_list_foreach(*recurrences, (GFunc)g_free, NULL);
896 g_list_free(*recurrences);
Date and Time handling routines.
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.
#define PERR(format, args...)
Log a serious error.
Account handling public routines.
void gnc_dow_abbrev(gchar *buf, int buf_len, int dow)
Localized DOW abbreviation.
time64 gdate_to_time64(GDate d)
Turns a GDate into a time64, returning the first second of the day.
#define MAX_DATE_LENGTH
The maximum length of a string created by the date printers.
All type declarations for the whole Gnucash engine.
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...
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...