[patch 2/8] [recurrence.diff] Add the Recurrence data type to the
engine
c.shoemaker at cox.net
c.shoemaker at cox.net
Sat Oct 15 00:18:29 EDT 2005
* src/engine/Makefile.am | 2
* src/engine/Recurrence.c | 299 +++++++++++++++++++++++++++++
* src/engine/Recurrence.h | 121 ++++++++++++
* src/engine/test/Makefile.am | 2
* src/engine/test/test-recurrence.c | 383 ++++++++++++++++++++++++++++++++++++++
- Add the Recurrence data type to the engine
src/engine/Makefile.am | 2
src/engine/Recurrence.c | 298 +++++++++++++++++++++++++++++
src/engine/Recurrence.h | 121 +++++++++++
src/engine/test/Makefile.am | 2
src/engine/test/test-recurrence.c | 384 ++++++++++++++++++++++++++++++++++++++
5 files changed, 807 insertions(+)
Index: gnucash/src/engine/Makefile.am
===================================================================
--- gnucash.orig/src/engine/Makefile.am
+++ gnucash/src/engine/Makefile.am
@@ -13,6 +13,7 @@ AM_CFLAGS = \
libgncmod_engine_la_SOURCES = \
Account.c \
FreqSpec.c \
+ Recurrence.c \
Group.c \
Period.c \
Query.c \
@@ -69,6 +70,7 @@ gncincludedir = ${GNC_INCLUDE_DIR}
gncinclude_HEADERS = \
Account.h \
FreqSpec.h \
+ Recurrence.h \
GNCId.h \
Group.h \
Period.h \
Index: gnucash/src/engine/Recurrence.c
===================================================================
--- /dev/null
+++ gnucash/src/engine/Recurrence.c
@@ -0,0 +1,298 @@
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net> *
+ * This file is free software. See COPYING for details.
+ */
+
+#include "config.h"
+#include <time.h>
+#include <glib.h>
+#include <string.h>
+#include "Recurrence.h"
+#include "gnc-date.h"
+#include "qof.h"
+#include "gnc-engine.h"
+
+static QofLogModule log_module = GNC_MOD_ENGINE;
+
+static GDate invalid_gdate;
+
+/* Do not intl. These are used for xml storage. */
+static gchar *period_type_strings[NUM_PERIOD_TYPES] = {
+ "once", "day", "week", "month", "end of month",
+ "nth weekday", "last weekday", "year",
+};
+
+#define VALID_PERIOD_TYPE(pt) ((0 <= (pt)) && ((pt) < NUM_PERIOD_TYPES))
+
+PeriodType
+recurrenceGetPeriodType(const Recurrence *r)
+{
+ return r ? r->ptype : PERIOD_INVALID;
+}
+
+guint
+recurrenceGetMultiplier(const Recurrence *r)
+{
+ return r ? r->mult : 0;
+}
+
+GDate
+recurrenceGetDate(const Recurrence *r)
+{
+ return r ? r->start : invalid_gdate;
+}
+
+void
+recurrenceSet(Recurrence *r, guint16 mult, PeriodType pt, const GDate *_start)
+{
+ r->ptype = VALID_PERIOD_TYPE(pt) ? pt : PERIOD_MONTH;
+ r->mult = (pt == PERIOD_ONCE) ? 0 : (mult > 0 ? mult : 1);
+
+ if (_start && g_date_valid(_start))
+ r->start = *_start;
+ else
+ g_date_set_time(&r->start, time(NULL));
+
+ /* Some of the unusual period types also specify phase. For those
+ types, we ensure that the start date agrees with that phase. */
+ switch (r->ptype) {
+ case PERIOD_END_OF_MONTH:
+ g_date_set_day(&r->start, g_date_get_days_in_month
+ (g_date_get_month(&r->start),
+ g_date_get_year(&r->start)));
+ break;
+ case PERIOD_LAST_WEEKDAY: {
+ GDateDay dim;
+ dim = g_date_get_days_in_month(g_date_get_month(&r->start),
+ g_date_get_year(&r->start));
+ while (dim - g_date_get_day(&r->start) >=7)
+ g_date_add_days(&r->start, 7);
+ } break;
+ case PERIOD_NTH_WEEKDAY:
+ if ((g_date_get_day(&r->start)-1) / 7 == 4) /* Fifth week */
+ r->ptype = PERIOD_LAST_WEEKDAY;
+ break;
+ default: break;
+ }
+}
+
+/* nth_weekday_compare() is a helper function for the
+ PERIOD_{NTH,LAST}_WEEKDAY case. It returns the offset, in days,
+ from 'next' to the nth weekday specified by the 'start' date (and
+ the period type), in the same month as 'next'. A negative offset
+ means earlier than 'next'; a zero offset means 'next' *is* the nth
+ weekday in that month; a positive offset means later than
+ 'next'. */
+static gint
+nth_weekday_compare(const GDate *start, const GDate *next, PeriodType pt)
+{
+ GDateDay sd, nd;
+ gint matchday, dim;
+
+ nd = g_date_get_day(next);
+ sd = g_date_get_day(start);
+
+ /* matchday has a week part, capped at 3 weeks, and a day part,
+ capped at 7 days, so max(matchday) == 3*7 + 7 == 28. */
+ matchday = 7 * ((sd-1)/7 == 4 ? 3 : (sd-1)/7) +
+ (nd - g_date_get_weekday(next) + g_date_get_weekday(start) + 7) % 7;
+ /* That " + 7" is to avoid negative modulo in case nd < 6. */
+
+ dim = g_date_get_days_in_month(
+ g_date_get_month(next), g_date_get_year(next));
+ if ((dim - matchday) >= 7 && pt == PERIOD_LAST_WEEKDAY)
+ matchday += 7; /* Go to the fifth week, if needed */
+
+ return matchday - nd; /* Offset from 'next' to matchday */
+}
+
+
+/* This is the only real algorithm related to recurrences. It goes:
+ Step 1) Go forward one period from the reference date.
+ Step 2) Back up to align to the phase of the start date.
+*/
+void
+recurrenceNextInstance(const Recurrence *r, const GDate *ref, GDate *next)
+{
+ PeriodType pt;
+ const GDate *start;
+ guint mult;
+
+ g_return_if_fail(r && ref);
+ g_return_if_fail(g_date_valid(&r->start) && g_date_valid(ref));
+
+ /* If the ref date comes before the start date then the next
+ occurrence is always the start date, and we're done. */
+ start = &r->start;
+ if (g_date_compare(ref, start) < 0) {
+ g_date_set_julian(next, g_date_get_julian(start));
+ return;
+ }
+ g_date_set_julian(next, g_date_get_julian(ref)); /* start at refDate */
+
+ /* Step 1: move FORWARD one period, passing exactly one occurrence. */
+ mult = r->mult;
+ pt = r->ptype;
+ switch (pt) {
+ case PERIOD_YEAR:
+ mult *= 12; /* fall-through */
+ case PERIOD_MONTH:
+ case PERIOD_NTH_WEEKDAY:
+ case PERIOD_LAST_WEEKDAY:
+ case PERIOD_END_OF_MONTH:
+ /* Takes care of short months. */
+ if ( g_date_is_last_of_month(next) ||
+ ((pt == PERIOD_MONTH || pt == PERIOD_YEAR) &&
+ g_date_get_day(next) >= g_date_get_day(start)) ||
+ ((pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY) &&
+ nth_weekday_compare(start, next, pt) <= 0) )
+ g_date_add_months(next, mult);
+ else
+ /* one fewer month fwd because of the occurrence in this month */
+ g_date_add_months(next, mult - 1);
+ break;
+ case PERIOD_WEEK:
+ mult *= 7; /* fall-through */
+ case PERIOD_DAY:
+ g_date_add_days(next, mult);
+ break;
+ case PERIOD_ONCE:
+ g_date_clear(next, 1); /* We already caught the case where ref is */
+ return; /* earlier than start, so this is invalid. */
+ default:
+ PERR("Invalid period type");
+ }
+
+ /* Step 2: Back up to align to the base phase. To ensure forward
+ progress, we never subtract as much as we added (x % mult < mult). */
+ switch (pt) {
+ case PERIOD_YEAR:
+ case PERIOD_MONTH:
+ case PERIOD_NTH_WEEKDAY:
+ case PERIOD_LAST_WEEKDAY:
+ case PERIOD_END_OF_MONTH: {
+ guint dim, n_months;
+
+ n_months = 12 * (g_date_get_year(next) - g_date_get_year(start)) +
+ (g_date_get_month(next) - g_date_get_month(start));
+ g_date_subtract_months(next, n_months % mult);
+
+ /* Ok, now we're in the right month, so we just have to align
+ the day in one of the three possible ways. */
+ dim = g_date_get_days_in_month(g_date_get_month(next),
+ g_date_get_year(next));
+ if (pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY)
+ g_date_add_days(next, nth_weekday_compare(start, next, pt));
+ else if (pt == PERIOD_END_OF_MONTH || g_date_get_day(start) >= dim)
+ g_date_set_day(next, dim); /* last day in the month */
+ else
+ g_date_set_day(next, g_date_get_day(start)); /*same day as start*/
+
+ } break;
+ case PERIOD_WEEK:
+ case PERIOD_DAY:
+ g_date_subtract_days(next, g_date_days_between(start, next) % mult);
+ break;
+ default:
+ PERR("Invalid period type");
+ }
+}
+
+/* Zero-based index */
+void
+recurrenceNthInstance(const Recurrence *r, guint n, GDate *date)
+{
+ GDate ref;
+ guint i;
+
+ for (*date = ref = r->start, i = 0; i < n; i++) {
+ recurrenceNextInstance(r, &ref, date);
+ ref = *date;
+ }
+}
+
+void
+recurrenceListNextInstance(const GList *rlist, const GDate *ref, GDate *next)
+{
+ const GList *iter;
+ GDate nextSingle; /* The next date for an individual recurrence */
+
+ g_return_if_fail(rlist && ref && next && g_date_valid(ref));
+
+ g_date_clear(next, 1);
+ for (iter = rlist; iter; iter = iter->next) {
+ const Recurrence *r = iter->data;
+
+ recurrenceNextInstance(r, ref, &nextSingle);
+ if (!g_date_valid(&nextSingle)) continue;
+
+ if (g_date_valid(next))
+ g_date_order(next, &nextSingle); /* swaps dates if needed */
+ else
+ *next = nextSingle; /* first date is always earliest so far */
+ }
+}
+
+/* Caller owns the returned memory */
+gchar *
+recurrenceToString(const Recurrence *r)
+{
+ gchar *tmpDate;
+ gchar *tmpPeriod, *ret;
+
+ g_return_val_if_fail(g_date_valid(&r->start), NULL);
+ tmpDate = g_new0(gchar, MAX_DATE_LENGTH+1);
+ g_date_strftime(tmpDate, MAX_DATE_LENGTH, "%x", &r->start);
+
+ if (r->ptype == PERIOD_ONCE) {
+ ret = g_strdup_printf("once on %s", tmpDate);
+ goto done;
+ }
+
+ tmpPeriod = period_type_strings[r->ptype];
+ if (r->mult > 1)
+ ret = g_strdup_printf("Every %d %ss beginning %s",
+ r->mult, tmpPeriod, tmpDate);
+ else
+ ret = g_strdup_printf("Every %s beginning %s",
+ tmpPeriod, tmpDate);
+done:
+ g_free(tmpDate);
+ return ret;
+}
+
+/* caller owns the returned memory */
+gchar *
+recurrenceListToString(const GList *r)
+{
+ const GList *iter;
+ GString *str;
+ gchar *s;
+ g_return_val_if_fail(r, NULL);
+
+ str = g_string_new("");
+ for(iter = r; iter; iter = iter->next){
+ s = recurrenceToString((Recurrence *)iter->data);
+ g_string_append(str, s);
+ g_string_append(str, " + ");
+ g_free(s);
+ }
+ g_string_truncate(str, str->len - 3); /* kill the last " + " */
+ return g_string_free(str, FALSE);
+}
+
+gchar *
+recurrencePeriodTypeToString(PeriodType pt)
+{
+ return VALID_PERIOD_TYPE(pt) ? g_strdup(period_type_strings[pt]) : NULL;
+}
+
+PeriodType
+recurrencePeriodTypeFromString(const gchar *str)
+{
+ int i;
+
+ for (i = 0; i < NUM_PERIOD_TYPES; i++)
+ if (safe_strcmp(period_type_strings[i], str) == 0)
+ return i;
+ return -1;
+}
Index: gnucash/src/engine/Recurrence.h
===================================================================
--- /dev/null
+++ gnucash/src/engine/Recurrence.h
@@ -0,0 +1,121 @@
+/* Recurrence.h:
+ *
+ * A Recurrence represents the periodic occurrence of dates, with a
+ * beginning point. For example, "Every Friday, beginning April 15,
+ * 2005" or "The 1st of every 3rd month, beginning April 1, 2001."
+ *
+ * Technically, a Recurrence can also represent certain useful
+ * "almost periodic" date sequences. For example, "The last day of
+ * every month, beginning Feb. 28, 2005."
+ *
+ * The main operation you can perform on a Recurrence is to find the
+ * earliest date in the sequence of occurrences that is after some
+ * specified date (often the "previous" occurrence).
+ *
+ * In addition, you can use a GList of Recurrences to represent a
+ * sequence containing all the dates in each Recurrence in the list,
+ * and perform the same "next instance" computation for this
+ * sequence.
+ *
+ * Note: Recurrence is similar to FreqSpec, but it represents a
+ * broader concept than FreqSpec (because it also represents the
+ * beginning of the recurrence).
+ *
+ * Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ * This file is free software. See COPYING for details.
+ */
+
+#ifndef RECURRENCE_H
+#define RECURRENCE_H
+
+#include <glib.h>
+
+typedef enum {
+ PERIOD_ONCE, /* Not a true period at all, but convenient here. */
+ PERIOD_DAY,
+ PERIOD_WEEK,
+ PERIOD_MONTH,
+ PERIOD_END_OF_MONTH, /* This is actually a period plus a phase. */
+ PERIOD_NTH_WEEKDAY, /* Also a phase, e.g. Second Tueday. */
+ PERIOD_LAST_WEEKDAY, /* Also a phase. */
+ PERIOD_YEAR,
+ NUM_PERIOD_TYPES,
+ PERIOD_INVALID = -1,
+} PeriodType;
+
+/* Recurrences represent both the phase and period of a recurring event. */
+
+typedef struct {
+ GDate start; /* First date in the recurrence; specifies phase. */
+ PeriodType ptype; /* see PeriodType enum */
+ guint16 mult; /* a period multiplier */
+} Recurrence;
+
+
+/* recurrenceSet() will enforce internal consistency by overriding
+ inconsistent inputs so that 'r' will _always_ end up being valid
+ recurrence.
+
+ - if the period type is invalid, PERIOD_MONTH is used.
+
+ - if the period type is PERIOD_ONCE, then mult is ignored,
+ otherwise, if mult is zero, then mult of 1 is used.
+
+ - if the date is invalid, the current date is used.
+
+ - if the period type specifies phase, the date is made to agree
+ with that phase:
+
+ - for PERIOD_END_OF_MONTH, the last day of date's month is used.
+
+ - for PERIOD_NTH_WEEKDAY, a fifth weekday converts to a
+ PERIOD_LAST_WEEKDAY
+
+ - for PERIOD_LAST_WEEKDAY, the last day in date's month with
+ date's day-of-week is used.
+
+*/
+void recurrenceSet(Recurrence *r, guint16 mult, PeriodType pt,
+ const GDate *date);
+
+/* get the fields */
+PeriodType recurrenceGetPeriodType(const Recurrence *r);
+guint recurrenceGetMultiplier(const Recurrence *r);
+GDate recurrenceGetDate(const Recurrence *r);
+
+/* Get the occurence immediately after refDate.
+ *
+ * This function has strict and precise post-conditions:
+ *
+ * Given a valid recurrence and a valid 'refDate', 'nextDate' will be
+ * *IN*valid IFF the period_type is PERIOD_ONCE, and 'refDate' is
+ * later-than or equal to the single occurrence (start_date).
+ *
+ * A valid 'nextDate' will _always_ be:
+ * - strictly later than the 'refDate', AND
+ * - later than or equal to the start date of the recurrence, AND
+ * - exactly an integral number of periods away from the start date
+ *
+ * Furthermore, there will be no date _earlier_ than 'nextDate' for
+ * which the three things above are true.
+ *
+ */
+void recurrenceNextInstance(const Recurrence *r, const GDate *refDate,
+ GDate *nextDate);
+
+/* Zero-based. n == 1 gets the instance after the start date. */
+void recurrenceNthInstance(const Recurrence *r, guint n, GDate *date);
+
+/* Get the earliest of the next occurances -- a "composite" recurrence */
+void recurrenceListNextInstance(const GList *r, const GDate *refDate,
+ GDate *nextDate);
+
+/* These two functions are only for xml storage, not user presentation. */
+gchar *recurrencePeriodTypeToString(PeriodType pt);
+PeriodType recurrencePeriodTypeFromString(const gchar *str);
+
+/* For debugging. Caller owns the returned string. Not intl. */
+gchar *recurrenceToString(const Recurrence *r);
+gchar *recurrenceListToString(const GList *rlist);
+
+#endif /* RECURRENCE_H */
Index: gnucash/src/engine/test/Makefile.am
===================================================================
--- gnucash.orig/src/engine/test/Makefile.am
+++ gnucash/src/engine/test/Makefile.am
@@ -41,6 +41,7 @@ TESTS = \
test-transaction-reversal \
test-transaction-voiding \
test-freq-spec \
+ test-recurrence \
test-scm-query \
test-book-merge
@@ -63,6 +64,7 @@ noinst_PROGRAMS = \
test-commodities \
test-date \
test-freq-spec \
+ test-recurrence \
test-guid \
test-group-vs-book \
test-load-engine \
Index: gnucash/src/engine/test/test-recurrence.c
===================================================================
--- /dev/null
+++ gnucash/src/engine/test/test-recurrence.c
@@ -0,0 +1,384 @@
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ * This file is free software. See COPYING for details. */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <glib.h>
+
+#include "test-stuff.h"
+#include "FreqSpec.h"
+#include "Recurrence.h"
+#include "gnc-engine.h"
+#include "qofbook.h"
+
+static QofBook *book;
+static FreqSpec *fs;
+//#define FREQSPECTEST
+
+static void check_valid(GDate *next, GDate *ref, GDate *start,
+ guint16 mult, PeriodType pt)
+{
+ gboolean valid;
+ gint startToNext;
+
+ valid = g_date_valid(next);
+ if (pt == PERIOD_ONCE && g_date_compare(start, ref) <= 0)
+ do_test(!valid, "incorrectly valid");
+ else
+ do_test(valid, "incorrectly invalid");
+
+ if (!valid) return;
+
+ // FreqSpec.h does claim to offer this.
+ do_test(g_date_compare(ref, next) < 0,
+ "next date not strictly later than ref date");
+ startToNext = g_date_get_julian(next) - g_date_get_julian(start);
+
+ // FreqSpec *doesn't* offer beginning dates.
+#ifndef FREQSPECTEST
+ do_test(startToNext >= 0, "next date is before start date");
+#endif
+
+ // Phase test
+ switch (pt) {
+ case PERIOD_YEAR:
+ do_test((g_date_get_year(next) - g_date_get_year(start)) % mult == 0,
+ "year period phase wrong"); // redundant
+ mult *= 12;
+ // fall-through
+ case PERIOD_END_OF_MONTH:
+#ifdef FREQSPECTEST
+ return; // FreqSpec doesn't have this case
+#endif
+ if (pt == PERIOD_END_OF_MONTH)
+ do_test(g_date_is_last_of_month(next), "end of month phase wrong");
+ // fall-through
+ case PERIOD_LAST_WEEKDAY:
+ case PERIOD_NTH_WEEKDAY:
+ case PERIOD_MONTH: {
+ gint monthdiff;
+ GDateDay day_start, day_next;
+
+ monthdiff = (g_date_get_month(next) - g_date_get_month(start)) +
+ 12 * (g_date_get_year(next) - g_date_get_year(start));
+ do_test(monthdiff % mult == 0, "month or year phase wrong");
+
+ if (pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY) {
+ guint sweek, nweek;
+
+ do_test(g_date_get_weekday(next) == g_date_get_weekday(start),
+ "weekday phase wrong");
+ sweek = (g_date_get_day(start)-1) / 7;
+ nweek = (g_date_get_day(next)-1) / 7;
+
+ /* 3 cases: either the weeks agree, OR 'next' didn't have
+ 5 of the weekday that 'start' did, so it's only the
+ 4th, OR 'start' didn't have 5 of the weekday that
+ 'next' does and we want the LAST weekday, so it's the
+ 5th of that weekday */
+ do_test(sweek == nweek ||
+ (sweek == 4 && nweek == 3 && (g_date_get_day(next) + 7) >
+ g_date_get_days_in_month(
+ g_date_get_month(next), g_date_get_year(next))) ||
+ (sweek == 3 && nweek == 4 && (pt == PERIOD_LAST_WEEKDAY)),
+ "week of month phase wrong");
+
+ } else {
+ day_start = g_date_get_day(start);
+ day_next = g_date_get_day(next);
+ if (day_start < 28)
+ do_test(day_start == day_next, "dom don't match");
+ else if (pt != PERIOD_END_OF_MONTH) {
+ // the end of month case was already checked above. near
+ // the end of the month, the days should still agree,
+ // unless they can't because of a short month.
+ do_test(day_start == day_next || g_date_is_last_of_month(next),
+ "dom don't match and next is not eom");
+ }
+ }
+ }
+ break;
+ case PERIOD_WEEK:
+ mult *= 7;
+ // fall-through
+ case PERIOD_DAY:
+ do_test((startToNext % mult) == 0, "week or day period phase wrong");
+ break;
+ case PERIOD_ONCE:
+ do_test(startToNext == 0, "period once not on start date");
+ break;
+ default:
+ do_test(FALSE, "invalid PeriodType");
+ }
+
+}
+
+#ifdef FREQSPECTEST
+static void convert_pt_to_fs(FreqSpec *fs, guint mult,
+ PeriodType pt, GDate *start)
+{
+ switch (pt) {
+ case PERIOD_ONCE:
+ xaccFreqSpecSetOnceDate(fs, start);
+ break;
+ case PERIOD_DAY:
+ xaccFreqSpecSetDaily(fs, start, mult);
+ break;
+ case PERIOD_WEEK:
+ xaccFreqSpecSetWeekly(fs, start, mult);
+ break;
+ case PERIOD_MONTH:
+ xaccFreqSpecSetMonthly(fs, start, mult);
+ break;
+ case PERIOD_END_OF_MONTH:
+ // not handled
+ case PERIOD_NTH_WEEKDAY:
+ break;
+ case PERIOD_YEAR:
+ xaccFreqSpecSetMonthly(fs, start, 12*mult);
+ break;
+ default:
+ ;
+ }
+}
+#endif
+
+#define NUM_DATES_TO_TEST 300
+#define NUM_DATES_TO_TEST_REF 300
+#define NUM_MULT_TO_TEST 10
+#define JULIAN_START 2003*365 // years have to be < 10000
+
+/* Mult of zero is usually not valid, but it gets regularized to 1, so
+ the effect is just that we end up testing mult of 1 twice, plus the
+ regularization. */
+static void test_all()
+{
+ Recurrence r;
+ GDate d_start, d_start_reg;
+ GDate d_ref, d_next;
+ guint16 mult, mult_reg;
+ PeriodType pt, pt_reg;
+ gint32 j1, j2;
+ gint i_ref;
+
+ for (pt = PERIOD_ONCE; pt < NUM_PERIOD_TYPES; pt++) {
+ for (j1 = JULIAN_START; j1 < JULIAN_START + NUM_DATES_TO_TEST; j1++) {
+ g_date_set_julian(&d_start, j1);
+ for (i_ref = 0; i_ref < NUM_DATES_TO_TEST_REF; i_ref++) {
+ j2 = (guint32) get_random_int_in_range(1, 1 << 19);
+ g_date_set_julian(&d_ref, j2);
+
+ for (mult = 0; mult < NUM_MULT_TO_TEST; mult++) {
+ recurrenceSet(&r, mult, pt, &d_start);
+ pt_reg = recurrenceGetPeriodType(&r);
+ d_start_reg = recurrenceGetDate(&r);
+ mult_reg = recurrenceGetMultiplier(&r);
+
+ recurrenceNextInstance(&r, &d_ref, &d_next);
+#ifdef FREQSPECTEST
+ convert_pt_to_fs(fs, mult_reg, pt_reg, &d_start_reg);
+ xaccFreqSpecGetNextInstance(fs, &d_ref, &d_next);
+#endif
+ check_valid(&d_next, &d_ref, &d_start_reg,
+ mult_reg, pt_reg);
+
+ }
+ }
+ }
+ }
+}
+
+static gboolean test_equal(GDate *d1, GDate *d2)
+{
+ if (!do_test(g_date_compare(d1, d2) == 0, "dates don't match")) {
+ gchar s1[21];
+ gchar s2[21];
+ g_date_strftime(s1, 20, "%x", d1);
+ g_date_strftime(s2, 20, "%x", d2);
+
+ printf("%s != %s\n", s1, s2);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+
+static void test_specific(PeriodType pt, guint16 mult,
+ GDateMonth sm, GDateDay sd, GDateYear sy,
+ GDateMonth rm, GDateDay rd, GDateYear ry,
+ GDateMonth nm, GDateDay nd, GDateYear ny)
+{
+ GDate start;
+ GDate ref, next, true_next;
+ Recurrence r;
+
+ g_date_set_dmy(&start, sd, sm, sy);
+ g_date_set_dmy(&ref, rd, rm, ry);
+ g_date_set_dmy(&true_next, nd, nm, ny);
+
+
+ recurrenceSet(&r, mult, pt, &start);
+ recurrenceNextInstance(&r, &ref, &next);
+#ifdef FREQSPECTEST
+ convert_pt_to_fs(fs, mult, pt, &start);
+ xaccFreqSpecGetNextInstance(fs, &ref, &next);
+#endif
+
+ check_valid(&next, &ref, &start, mult, pt);
+ if (!test_equal(&next, &true_next)) {
+ gchar s1[21], s2[21], s3[21];
+ g_date_strftime(s1, 20, "%x", &start);
+ g_date_strftime(s2, 20, "%x", &ref);
+ g_date_strftime(s3, 20, "%x", &true_next);
+ printf("pt = %d; mult = %d; start = %s; ref = %s; true_next = %s\n",
+ pt, mult, s1, s2, s3);
+ }
+}
+
+#if 0
+static void test_nth(GDateMonth sm, GDateDay sd, GDateYear sy,
+ GDateMonth nm, GDateDay nd, GDateYear ny,
+ gint diff, PeriodType pt)
+{
+ GDate start, next;
+ gint d;
+
+ g_date_set_dmy(&start, sd, sm, sy);
+ g_date_set_dmy(&next, nd, nm, ny);
+
+ d = nth_weekday_compare(&start, &next, pt);
+ do_test(d == diff, "nth");
+}
+
+static void test_nth_compare()
+{
+ test_nth(4,1,2005, 4,2,2005, -1, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 4,4,2005, -3, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 4,7,2005, -6, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 4,8,2005, -7, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 4,14,2005, -13, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 4,30,2005, -29, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 5,1,2005, 5, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 5,5,2005, 1, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 5,6,2005, 0, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 5,7,2005, -1, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 5,8,2005, -2, PERIOD_NTH_WEEKDAY);
+ test_nth(4,1,2005, 5,21,2005, -15, PERIOD_NTH_WEEKDAY);
+
+
+ test_nth(4,6,2005, 4,1,2005, 5, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,4,2005, 2, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,6,2005, 0, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,9,2005, -3, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,11,2005, -5, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,13,2005, -7, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,14,2005, -8, PERIOD_NTH_WEEKDAY);
+ test_nth(4,6,2005, 4,29,2005, -23, PERIOD_NTH_WEEKDAY);
+
+ test_nth(4,12,2005, 4,1,2005, 11, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,4,2005, 8, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,11,2005, 1, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,12,2005, 0, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,13,2005, -1, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,17,2005, -5, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,19,2005, -7, PERIOD_NTH_WEEKDAY);
+ test_nth(4,12,2005, 4,28,2005, -16, PERIOD_NTH_WEEKDAY);
+
+ test_nth(4,29,2005, 4,30,2005, -1, PERIOD_LAST_WEEKDAY);
+ test_nth(4,29,2005, 5,1,2005, 26, PERIOD_LAST_WEEKDAY);
+ test_nth(4,29,2005, 7,9,2005, 20, PERIOD_LAST_WEEKDAY);
+ test_nth(4,29,2005, 7,31,2005, -2, PERIOD_LAST_WEEKDAY);
+
+ test_nth(4,28,2005, 4,30,2005, -2, PERIOD_LAST_WEEKDAY);
+ test_nth(4,28,2005, 5,1,2005, 25, PERIOD_LAST_WEEKDAY);
+ test_nth(4,28,2005, 7,9,2005, 19, PERIOD_LAST_WEEKDAY);
+ test_nth(4,28,2005, 7,31,2005, -3, PERIOD_LAST_WEEKDAY);
+ test_nth(4,28,2005, 9,21,2005, 8, PERIOD_LAST_WEEKDAY);
+
+}
+#endif
+static void test_some()
+{
+ test_specific(PERIOD_NTH_WEEKDAY, 1, 4,1,2005, 4,2,2005, 5,6,2005);
+ test_specific(PERIOD_NTH_WEEKDAY, 1, 7,14,2005, 11,15,2005, 12,8,2005);
+ test_specific(PERIOD_NTH_WEEKDAY, 1, 7,14,2005, 11,5,2005, 11,10,2005);
+ test_specific(PERIOD_NTH_WEEKDAY, 1, 4,1,2005, 4,2,2005, 5,6,2005);
+ test_specific(PERIOD_NTH_WEEKDAY, 1, 4,1,2005, 4,2,2005, 5,6,2005);
+
+ test_specific(PERIOD_LAST_WEEKDAY, 1, 4,29,2005, 4,30,2005, 5,27,2005);
+ test_specific(PERIOD_LAST_WEEKDAY, 1, 4,29,2005, 5,1,2005, 5,27,2005);
+ test_specific(PERIOD_LAST_WEEKDAY, 1, 4,29,2005, 7,9,2005, 7,29,2005);
+ test_specific(PERIOD_LAST_WEEKDAY, 1, 4,29,2005, 6,30,2005, 7,29,2005);
+ test_specific(PERIOD_LAST_WEEKDAY, 1, 4,29,2005, 7,31,2005, 8,26,2005);
+
+ test_specific(PERIOD_NTH_WEEKDAY, 2, 4,27,2005, 4,27,2005, 6,22,2005);
+ //exit(1);
+ //return;
+ test_specific(PERIOD_YEAR, 3, 9,8,838, 6,30,1094, 9,8,1096);
+ test_specific(PERIOD_YEAR, 2, 9,8,838, 6,30,1094, 9,8,1094);
+ test_specific(PERIOD_YEAR, 1, 1,10,1000, 1,5,1002, 1,10,1002);
+ //return;
+ test_specific(PERIOD_MONTH, 1, 1,12,1, 2,6,1, 2,12,1);
+
+ test_specific(PERIOD_MONTH, 1, 1,12,1, 2,12,1, 3,12,1);
+ test_specific(PERIOD_MONTH, 1, 1,12,1, 2,20,1, 3,12,1);
+ test_specific(PERIOD_MONTH, 1, 1,30,1, 2,28,1, 3,30,1);
+ test_specific(PERIOD_MONTH, 1, 1,30,1, 2,27,1, 2,28,1);
+ test_specific(PERIOD_MONTH, 1, 2,28,1, 3,30,1, 4,28,1);
+
+#ifdef FREQSPECTEST
+ test_specific(PERIOD_MONTH, 3, 12, 12, 1, 2, 1, 1, 3, 12, 1);
+#else
+ test_specific(PERIOD_END_OF_MONTH, 1, 2,28,1, 3,30,1, 3,31,1);
+ test_specific(PERIOD_END_OF_MONTH, 5, 4,30,1, 4,21,1, 4,30,1);
+ test_specific(PERIOD_END_OF_MONTH, 5, 2,28,1, 5,21,1, 7,31,1);
+#endif
+ test_specific(PERIOD_YEAR, 7, 6,8,199, 9,10,1338, 6,8,1340);
+ test_specific(PERIOD_YEAR, 2, 9,8,838, 6,30,1094, 9,8,1094);
+
+ test_specific(PERIOD_YEAR,1, 5,2,13, 1,11,101, 5,2,101);
+ test_specific(PERIOD_DAY, 7, 4,1,2000, 4,8,2000, 4,15,2000);
+}
+
+static void test_use()
+{
+ Recurrence *r;
+
+ r = g_new(Recurrence, 1);
+ do_test(r != NULL, "allocation");
+ g_free(r);
+}
+
+static void test_main()
+{
+
+ book = qof_book_new ();
+ fs = xaccFreqSpecMalloc(book);
+
+ test_use();
+
+ test_some();
+
+ test_all();
+
+ xaccFreqSpecFree(fs);
+ qof_book_destroy (book);
+}
+
+
+int
+main (int argc, char **argv)
+{
+ g_log_set_always_fatal( G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING );
+
+#if 0
+ set_success_print(TRUE);
+#endif
+
+ test_main();
+
+ print_test_results();
+ return get_rv();
+}
--
More information about the gnucash-patches
mailing list