[Gnucash-changes] Chris Shoemaker's patch to add a "Recurrence" data type to the engine.

David Hampton hampton at cvs.gnucash.org
Mon Oct 24 23:48:54 EDT 2005


Log Message:
-----------
Chris Shoemaker's patch to add a "Recurrence" data type to the engine.
This should eventually subsume the FreqSpec data type.

Tags:
----
gnucash-gnome2-dev

Modified Files:
--------------
    gnucash/src/engine:
        Makefile.am
    gnucash/src/engine/test:
        Makefile.am

Added Files:
-----------
    gnucash/src/engine:
        Recurrence.c
        Recurrence.h
    gnucash/src/engine/test:
        test-recurrence.c

Revision Data
-------------
--- /dev/null
+++ src/engine/Recurrence.c
@@ -0,0 +1,314 @@
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652
+ * Boston, MA  02111-1307,  USA       gnu at gnu.org
+ */
+
+#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: Makefile.am
===================================================================
RCS file: /home/cvs/cvsroot/gnucash/src/engine/Makefile.am,v
retrieving revision 1.94.2.19
retrieving revision 1.94.2.20
diff -Lsrc/engine/Makefile.am -Lsrc/engine/Makefile.am -u -r1.94.2.19 -r1.94.2.20
--- src/engine/Makefile.am
+++ src/engine/Makefile.am
@@ -110,6 +110,7 @@
 libgncmod_engine_la_SOURCES = \
   Account.c \
   FreqSpec.c \
+  Recurrence.c \
   Group.c \
   Period.c \
   Query.c \
@@ -146,6 +147,7 @@
 gncinclude_HEADERS = \
   Account.h \
   FreqSpec.h \
+  Recurrence.h \
   GNCId.h \
   Group.h \
   Period.h \
--- /dev/null
+++ src/engine/Recurrence.h
@@ -0,0 +1,137 @@
+/* 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 program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652
+ * Boston, MA  02111-1307,  USA       gnu at gnu.org
+ */
+
+#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: Makefile.am
===================================================================
RCS file: /home/cvs/cvsroot/gnucash/src/engine/test/Makefile.am,v
retrieving revision 1.29.4.9
retrieving revision 1.29.4.10
diff -Lsrc/engine/test/Makefile.am -Lsrc/engine/test/Makefile.am -u -r1.29.4.9 -r1.29.4.10
--- src/engine/test/Makefile.am
+++ src/engine/test/Makefile.am
@@ -43,6 +43,7 @@
   test-transaction-reversal \
   test-transaction-voiding \
   test-freq-spec \
+  test-recurrence \
   test-scm-query \
   test-book-merge 
   
@@ -65,6 +66,7 @@
   test-commodities \
   test-date \
   test-freq-spec \
+  test-recurrence \
   test-guid \
   test-group-vs-book \
   test-load-engine \
--- /dev/null
+++ src/engine/test/test-recurrence.c
@@ -0,0 +1,401 @@
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652
+ * Boston, MA  02111-1307,  USA       gnu at gnu.org
+ */
+
+#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-changes mailing list