diff --git a/ChangeLog b/ChangeLog index f8d76bd6b3..8525779566 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +2003-07-10 Derek Atkins + + * src/import-export/import-parse.[ch]: routines to parse numbers + and dates in a mostly-locale-agnostic way. + * src/import-export/import-format-dialog.c: a dialog to ask the user + to choose a format, when parsing just isn't enough. + * src/import-export/generic-import.glade: add the format_match dialog + * src/import-export/test/test-import-parse.c: test the import-parse + routines. + 2003-07-09 Derek Atkins * configure.in: change (and comment out) checks for pthreads diff --git a/src/import-export/Makefile.am b/src/import-export/Makefile.am index 055518a334..905bad12d7 100644 --- a/src/import-export/Makefile.am +++ b/src/import-export/Makefile.am @@ -6,7 +6,9 @@ libgncmod_generic_import_la_SOURCES = \ import-account-matcher.c \ import-commodity-matcher.c \ import-backend.c \ + import-format-dialog.c \ import-match-picker.c \ + import-parse.c \ import-utilities.c \ import-settings.c \ import-match-map.c \ @@ -15,7 +17,8 @@ libgncmod_generic_import_la_SOURCES = \ gncincludedir = ${GNC_INCLUDE_DIR} gncinclude_HEADERS = \ - import-match-map.h + import-match-map.h \ + import-parse.h noinst_HEADERS = \ import-account-matcher.h \ @@ -68,16 +71,24 @@ EXTRA_DIST = \ .cvsignore \ generic-import-design.txt -#noinst_DATA = .scm-links +if GNUCASH_SEPARATE_BUILDDIR +#For compiling +SCM_FILE_LINKS = +#For executing test cases +SCM_FILE_LINKS += generic-import.scm +endif -#.scm-links: -# rm -f g-wrapped gnucash import-export qif-import -# ln -sf . qif-import # to fix (load "qif-import/foo.scm") -# ln -sf . import-export -# ln -sf . gnucash -# ln -sf . g-wrapped -# touch .scm-links -DISTCLEANFILES = gnucash g-wrapped .scm-links import-export +noinst_DATA = .scm-links + +.scm-links: + rm -f g-wrapped gnucash generic-import import-export + ln -sf . import-export + ln -sf . gnucash + ln -sf . generic-import + ln -sf . g-wrapped + touch .scm-links + +DISTCLEANFILES = gnucash generic-import g-wrapped .scm-links import-export # FIXME remove this when qif-io-core is finished DIST_SUBDIRS = binary-import qif-import qif-io-core ofx test hbci log-replay diff --git a/src/import-export/generic-import.glade b/src/import-export/generic-import.glade index 9f94b2c874..5935092ae0 100644 --- a/src/import-export/generic-import.glade +++ b/src/import-export/generic-import.glade @@ -1092,4 +1092,140 @@ click "Ok". + + GnomeDialog + format_picker + Choose a format + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_CENTER + True + False + False + False + False + False + + + GtkVBox + GnomeDialog:vbox + dialog-vbox4 + False + 8 + + 4 + True + True + + + + GtkHButtonBox + GnomeDialog:action_area + dialog-action_area5 + GTK_BUTTONBOX_END + 8 + 85 + 27 + 7 + 0 + + 0 + False + True + GTK_PACK_END + + + + GtkButton + button79 + True + True + GNOME_STOCK_BUTTON_OK + + + + + GtkFrame + frame2 + + 0 + GTK_SHADOW_ETCHED_IN + + 0 + True + True + + + + GtkVBox + vbox8 + 5 + False + 15 + + + GtkLabel + msg_label + + GTK_JUSTIFY_CENTER + True + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GtkHBox + hbox1 + False + 3 + + 0 + True + True + + + + GtkLabel + label847791 + + GTK_JUSTIFY_LEFT + False + 1 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GtkHBox + menu_box + False + 0 + + 0 + True + True + + + + Placeholder + + + + + + + + diff --git a/src/import-export/import-format-dialog.c b/src/import-export/import-format-dialog.c new file mode 100644 index 0000000000..c6eb2dba2e --- /dev/null +++ b/src/import-export/import-format-dialog.c @@ -0,0 +1,119 @@ +/* + * import-format-dialog.c -- provides a UI to ask for users to resolve ambiguities. + * + * Created by: Derek Atkins + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "import-parse.h" +#include "dialog-utils.h" +#include "gnc-ui-util.h" + +#define MAX_CHOICES 6 + +static void +choice_option_changed (GtkWidget *widget, gint index, gpointer index_p) +{ + gint *my_index = index_p; + *my_index = index; +} + +static GncImportFormat +add_menu_and_run_dialog(GtkWidget *dialog, GtkWidget *menu_box, GncImportFormat fmt) +{ + GtkWidget *menu; + gint index = 0, count = 0; + GncImportFormat formats[MAX_CHOICES]; + GNCOptionInfo menus[MAX_CHOICES]; + + memset(&menus, 0, sizeof(menus)); + + if (fmt & GNCIF_NUM_PERIOD) { + formats[count] = GNCIF_NUM_PERIOD; + menus[count].name = _("Period: 123,456.78"); + menus[count].callback = choice_option_changed; + menus[count].user_data = &index; + count++; + } + + if (fmt & GNCIF_NUM_COMMA) { + formats[count] = GNCIF_NUM_COMMA; + menus[count].name = _("Comma: 123.456,78"); + menus[count].callback = choice_option_changed; + menus[count].user_data = &index; + count++; + } + + if (fmt & GNCIF_DATE_MDY) { + formats[count] = GNCIF_DATE_MDY; + menus[count].name = _("m/d/y"); + menus[count].callback = choice_option_changed; + menus[count].user_data = &index; + count++; + } + + if (fmt & GNCIF_DATE_DMY) { + formats[count] = GNCIF_DATE_DMY; + menus[count].name = _("d/m/y"); + menus[count].callback = choice_option_changed; + menus[count].user_data = &index; + count++; + } + + if (fmt & GNCIF_DATE_YMD) { + formats[count] = GNCIF_DATE_YMD; + menus[count].name = _("y/m/d"); + menus[count].callback = choice_option_changed; + menus[count].user_data = &index; + count++; + } + + if (fmt & GNCIF_DATE_YDM) { + formats[count] = GNCIF_DATE_YDM; + menus[count].name = _("y/d/m"); + menus[count].callback = choice_option_changed; + menus[count].user_data = &index; + count++; + } + + g_assert(count > 1); + menu = gnc_build_option_menu(menus, count); + gtk_box_pack_start(GTK_BOX(menu_box), menu, TRUE, TRUE, 0); + + gtk_widget_show_all(dialog); + gnome_dialog_run_and_close(GNOME_DIALOG(dialog)); + + return formats[index]; +} + +GncImportFormat +gnc_import_choose_fmt(const char* msg, GncImportFormat fmts, gpointer data) + +{ + GladeXML *xml; + GtkWidget *dialog; + GtkWidget *widget; + + g_return_val_if_fail(fmts, FALSE); + + /* if there is only one format availble, just return it */ + if (!(fmts & (fmts-1))) { + return fmts; + } + + xml = gnc_glade_xml_new("generic-import.glade", "format_picker"); + dialog = glade_xml_get_widget(xml, "format_picker"); + widget = glade_xml_get_widget(xml, "msg_label"); + gtk_label_set_text(GTK_LABEL(widget), msg); + + widget = glade_xml_get_widget(xml, "menu_box"); + return add_menu_and_run_dialog(dialog, widget, fmts); +} diff --git a/src/import-export/import-parse.c b/src/import-export/import-parse.c new file mode 100644 index 0000000000..6d40b0f196 --- /dev/null +++ b/src/import-export/import-parse.c @@ -0,0 +1,363 @@ +/* + * import-parse.c -- a generic "parser" API for importers.. Allows importers + * to parse dates and numbers, and provides a UI to ask for users to + * resolve ambiguities. + * + * Created by: Derek Atkins + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +/* For regex */ +#include +#include + +#include "gnc-engine-util.h" +#include "gnc-ui-util.h" + +#include "import-parse.h" + +static short module = MOD_IMPORT; + +/* numeric regular expressions */ +static regex_t decimal_radix_regex; +static regex_t comma_radix_regex; + +/* date regular expressions */ +static regex_t date_regex; +static regex_t date_mdy_regex; +static regex_t date_ymd_regex; + +static gboolean regex_compiled = FALSE; + +static void +compile_regex(void) +{ + int flags = REG_EXTENDED; + + /* compile the numeric regular expressions */ + regcomp(&decimal_radix_regex, + "^ *\\$?[+-]?\\$?[0-9]+ *$|^ *\\$?[+-]?\\$?[0-9]?[0-9]?[0-9]?(,[0-9][0-9][0-9])*(\\.[0-9]*)? *$|^ *\\$?[+-]?\\$?[0-9]+\\.[0-9]* *$", flags); + regcomp(&comma_radix_regex, + "^ *\\$?[+-]?\\$?[0-9]+ *$|^ *\\$?[+-]?\\$?[0-9]?[0-9]?[0-9]?(\\.[0-9][0-9][0-9])*(,[0-9]*)? *$|^ *\\$?[+-]?\\$?[0-9]+,[0-9]* *$", flags); + + /* compile the date-parsing regular expressions */ + regcomp(&date_regex, + "^ *([0-9]+) *[-/.'] *([0-9]+) *[-/.'] *([0-9]+).*$|^ *([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]).*$", flags); + regcomp(&date_mdy_regex, "([0-9][0-9])([0-9][0-9])([0-9][0-9][0-9][0-9])", flags); + regcomp(&date_ymd_regex, "([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])", flags); + + regex_compiled = TRUE; +} + +static gint +my_strntol(const char *str, int len) +{ + gint res = 0; + + g_return_val_if_fail(str, 0); + g_return_val_if_fail(len, 0); + + while(len--) { + + if (*str < '0' || *str > '9') { + str++; + continue; + } + + res *= 10; + res += *(str++) - '0'; + } + return res; +} + +/* + * based on a trio match (matches in spaces 1, 2, and 3), and a list + * of possible date formats, return the list of formats that this string + * could actually be. + */ +static GncImportFormat +check_date_format(const char * str, regmatch_t *match, GncImportFormat fmts) +{ + GncImportFormat res = 0; + int len0=0, len1=0, len2=0; + int val0=0, val1=0, val2=0; + + g_return_val_if_fail(match, res); + g_return_val_if_fail(fmts, res); + + /* Compute the lengths */ + len0 = match[1].rm_eo - match[1].rm_so; + len1 = match[2].rm_eo - match[2].rm_so; + len2 = match[3].rm_eo - match[3].rm_so; + + /* compute the numeric values */ + val0 = my_strntol(str + match[1].rm_so, len0); + val1 = my_strntol(str + match[2].rm_so, len1); + val2 = my_strntol(str + match[3].rm_so, len2); + + /* Filter out the possibilities. Hopefully only one will remain */ + + if (val0 > 12) import_clear_flag(fmts, GNCIF_DATE_MDY); + if (val0 > 31) import_clear_flag(fmts, GNCIF_DATE_DMY); + if (val0 < 1) { + import_clear_flag(fmts, GNCIF_DATE_DMY); + import_clear_flag(fmts, GNCIF_DATE_MDY); + } + + if (val1 > 12) { + import_clear_flag(fmts, GNCIF_DATE_DMY); + import_clear_flag(fmts, GNCIF_DATE_YMD); + } + if (val1 > 31) { + import_clear_flag(fmts, GNCIF_DATE_MDY); + import_clear_flag(fmts, GNCIF_DATE_YDM); + } + + if (val2 > 12) import_clear_flag(fmts, GNCIF_DATE_YDM); + if (val2 > 31) import_clear_flag(fmts, GNCIF_DATE_YMD); + if (val2 < 1) { + import_clear_flag(fmts, GNCIF_DATE_YMD); + import_clear_flag(fmts, GNCIF_DATE_YDM); + } + + /* if we've got a 4-character year, make sure the value is greater + * than 1930 and less than 2100. XXX: be sure to fix this by 2100! + */ + if (len0 == 4 && (val0 < 1930 || val0 > 2100)) { + import_clear_flag(fmts, GNCIF_DATE_YMD); + import_clear_flag(fmts, GNCIF_DATE_YDM); + } + if (len2 == 4 && (val2 < 1930 || val2 > 2100)) { + import_clear_flag(fmts, GNCIF_DATE_MDY); + import_clear_flag(fmts, GNCIF_DATE_DMY); + } + + /* If the first string has a length of only 1, then it is definitely + * not a year (although it could be a month or day). + */ + if (len0 == 1) { + import_clear_flag(fmts, GNCIF_DATE_YMD); + import_clear_flag(fmts, GNCIF_DATE_YDM); + } + + return fmts; +} + +GncImportFormat +gnc_import_test_numeric(const char* str, GncImportFormat fmts) +{ + GncImportFormat res = 0; + + g_return_val_if_fail(str, fmts); + + if (!regex_compiled) + compile_regex(); + + if ((fmts & GNCIF_NUM_PERIOD) && !regexec(&decimal_radix_regex, str, 0, NULL, 0)) + res |= GNCIF_NUM_PERIOD; + + if ((fmts & GNCIF_NUM_COMMA) && !regexec(&comma_radix_regex, str, 0, NULL, 0)) + res |= GNCIF_NUM_COMMA; + + return res; +} + + +GncImportFormat +gnc_import_test_date(const char* str, GncImportFormat fmts) +{ + regmatch_t match[5]; + GncImportFormat res = 0; + + g_return_val_if_fail(str, fmts); + g_return_val_if_fail(strlen(str) > 1, fmts); + + if (!regex_compiled) + compile_regex(); + + if (!regexec(&date_regex, str, 5, match, 0)) { + if (match[1].rm_so != -1) + res = check_date_format(str, match, fmts); + else { + /* Hmm, it matches XXXXXXXX, but is this YYYYxxxx or xxxxYYYY? + * let's try both ways and let the parser check that YYYY is + * valid. + */ + char temp[9]; + + g_return_val_if_fail(match[4].rm_so != -1, fmts); + g_return_val_if_fail(match[4].rm_eo - match[4].rm_so == 8, fmts); + + /* make a temp copy of the XXXXXXXX string */ + strncpy(temp, str+match[4].rm_so, 8); + temp[8] = '\0'; + + /* then check it against the ymd or mdy formats, as necessary */ + if (((fmts & GNCIF_DATE_YDM) || (fmts & GNCIF_DATE_YMD)) && + !regexec(&date_ymd_regex, temp, 4, match, 0)) + res |= check_date_format(temp, match, fmts); + + if (((fmts & GNCIF_DATE_DMY) || (fmts & GNCIF_DATE_MDY)) && + !regexec(&date_mdy_regex, temp, 4, match, 0)) + res |= check_date_format(temp, match, fmts); + } + } + + return res; +} + +gboolean +gnc_import_parse_numeric(const char* str, GncImportFormat fmt, gnc_numeric *val) +{ + g_return_val_if_fail(str, FALSE); + g_return_val_if_fail(val, FALSE); + g_return_val_if_fail(fmt, FALSE); + g_return_val_if_fail(!(fmt & (fmt-1)), FALSE); + + switch(fmt) { + case GNCIF_NUM_PERIOD: + return xaccParseAmountExtended(str, TRUE, '-', '.', ',', NULL, "$+", + val, NULL); + case GNCIF_NUM_COMMA: + return xaccParseAmountExtended(str, TRUE, '-', ',', '.', NULL, "$+", + val, NULL); + default: + PERR("invalid format: %d", fmt); + return FALSE; + } +} + +/* Handle y2k fixes, etc. + * obtaining the year "00", "2000", and "19100" all mean the same thing. + * output is an integer representing the year in the C.E. + */ +static int +fix_year(int y) +{ + /* two-digit numbers less than "70" are interpretted to be post-2000. */ + if (y < 70) + return (y + 2000); + + /* fix a common bug in printing post-2000 dates as 19100, etc. */ + if (y > 19000) + return (1900 + (y - 19000)); + + /* At this point we just want to make sure that this is a real date. + * y _should_ be a 'unix year' (which is the number of years since + * 1900), but it _COULD_ be a full date (1999, 2001, etc.). At some + * point in the future we can't tell the difference, but are we really + * going to care if this code fails in 3802? + */ + if (y < 1902) + return (y + 1900); + + /* y is good as it is */ + return y; +} + +gboolean +gnc_import_parse_date(const char *str, GncImportFormat fmt, Timespec *val) +{ + regmatch_t match[5]; + char temp[9]; + const char *datestr; + + int v0 = 0, v1=0, v2=0; + int m=0, d=0, y=0; + + g_return_val_if_fail(str, FALSE); + g_return_val_if_fail(val, FALSE); + g_return_val_if_fail(fmt, FALSE); + g_return_val_if_fail(!(fmt & (fmt-1)), FALSE); + + if (!regexec(&date_regex, str, 5, match, 0)) { + if (match[1].rm_so != -1) + datestr = str; + else { + /* date is of the form XXXXXXX; save it to a temp string and + * split it based on the format, either YYYYaabb or aabbYYYY + */ + g_return_val_if_fail(match[4].rm_so != -1, FALSE); + g_return_val_if_fail(match[4].rm_eo - match[4].rm_so == 8, FALSE); + + strncpy(temp, str+match[4].rm_so, 8); + temp[8] = '\0'; + + switch (fmt) { + case GNCIF_DATE_DMY: + case GNCIF_DATE_MDY: + g_return_val_if_fail(!regexec(&date_mdy_regex, temp, 4, match, 0), FALSE); + break; + case GNCIF_DATE_YMD: + case GNCIF_DATE_YDM: + g_return_val_if_fail(!regexec(&date_ymd_regex, temp, 4, match, 0), FALSE); + break; + default: + PERR("Invalid date format provided: %d", fmt); + return FALSE; + } + datestr = temp; + } + + /* datestr points to the date string, and match[123] contains the matches. */ + + if (match[1].rm_so == -1 || match[2].rm_so == -1 || match[3].rm_so == -1) { + PERR("can't interpret date %s", str); + return FALSE; + } + + /* grab the numerics */ + v0 = my_strntol(datestr + match[1].rm_so, match[1].rm_eo-match[1].rm_so); + v1 = my_strntol(datestr + match[2].rm_so, match[2].rm_eo-match[2].rm_so); + v2 = my_strntol(datestr + match[3].rm_so, match[3].rm_eo-match[3].rm_so); + + switch (fmt) { + case GNCIF_DATE_DMY: + if (v0 > 0 && v0 <= 31 && v1 > 0 && v1 <= 12 && v2 > 0) { + d = v0; m = v1; y = v2; + } else + PERR("format is d/m/y but date is %s", str); + break; + + case GNCIF_DATE_MDY: + if (v0 > 0 && v0 <= 12 && v1 > 0 && v1 <= 31 && v2 > 0) { + m = v0; d = v1; y = v2; + } else + PERR("format is m/d/y but date is %s", str); + break; + + case GNCIF_DATE_YMD: + if (v0 > 0 && v1 > 0 && v1 <= 12 && v2 > 0 && v2 <= 31) { + y = v0; m = v1; d = v2; + } else + PERR("format is y/m/d but date is %s", str); + break; + + case GNCIF_DATE_YDM: + if (v0 > 0 && v1 > 0 && v1 <= 31 && v2 > 0 && v2 <= 12) { + y = v0; d = v1; m = v2; + } else + PERR("format is y/d/m but date is %s", str); + break; + + default: + PERR("invalid date format: %d", fmt); + } + + if (!m || !d || !y) + return FALSE; + + y = fix_year(y); + *val = gnc_dmy2timespec(d, m, y); + return TRUE; + } + + return FALSE; +} diff --git a/src/import-export/import-parse.h b/src/import-export/import-parse.h new file mode 100644 index 0000000000..47edbcf955 --- /dev/null +++ b/src/import-export/import-parse.h @@ -0,0 +1,46 @@ +/* + * import-parse.h -- a generic "parser" API for importers.. Allows importers + * to parse dates and numbers, and provides a UI to ask for users to + * resolve ambiguities. + * + * Created by: Derek Atkins + * + */ + +#ifndef IMPORT_PARSE_H +#define IMPORT_PARSE_H + +#include "gnc-numeric.h" +#include "gnc-date.h" + +typedef enum { + /* number formats */ + GNCIF_NUM_PERIOD = (1 << 1), + GNCIF_NUM_COMMA = (1 << 2), + + /* date formats */ + GNCIF_DATE_MDY = (1 << 8), + GNCIF_DATE_DMY = (1 << 9), + GNCIF_DATE_YMD = (1 << 10), + GNCIF_DATE_YDM = (1 << 11) +} GncImportFormat; + + +GncImportFormat gnc_import_test_numeric(const char* str, GncImportFormat fmts); +GncImportFormat gnc_import_test_date(const char* str, GncImportFormat fmts); + + +GncImportFormat gnc_import_choose_fmt(const char* msg, GncImportFormat fmts, + gpointer user_data); + +gboolean gnc_import_parse_numeric(const char* str, GncImportFormat fmt, + gnc_numeric *val); +gboolean gnc_import_parse_date(const char *date, GncImportFormat fmt, + Timespec *val); + +/* Set and clear flags in bit-flags */ +#define import_set_flag(i,f) (i |= f) +#define import_clear_flag(i,f) (i &= ~f) + + +#endif /* IMPORT_PARSE_H */ diff --git a/src/import-export/test/Makefile.am b/src/import-export/test/Makefile.am index 26170a2d74..2ca002e708 100644 --- a/src/import-export/test/Makefile.am +++ b/src/import-export/test/Makefile.am @@ -1,6 +1,45 @@ -TESTS=test-link +AM_CFLAGS = \ + -I${top_srcdir}/src \ + -I${top_srcdir}/src/gnc-module \ + -I${top_srcdir}/src/test-core \ + -I${top_srcdir}/src/engine \ + -I${top_srcdir}/src/app-utils \ + -I${top_srcdir}/src/import-export \ + ${GUILE_INCS} \ + ${GLIB_CFLAGS} -noinst_PROGRAMS=test-link +LDADD = \ + ${top_builddir}/src/gnc-module/libgncmodule.la \ + ${top_builddir}/src/test-core/libgncmod-test.la \ + ../libgncmod-generic-import.la \ + ${GLIB_LIBS} -test_link_SOURCES=test-link.c -test_link_LDADD=../libgncmod-generic-import.la +TESTS = \ + test-link \ + test-import-parse + +GNC_TEST_DEPS := @GNC_TEST_SRFI_LOAD_CMD@ \ + --gnc-module-dir ${top_builddir}/src/gnc-module \ + --gnc-module-dir ${top_builddir}/src/engine \ + --gnc-module-dir ${top_builddir}/src/app-utils \ + --gnc-module-dir ${top_builddir}/src/import-export \ + --gnc-module-dir ${top_builddir}/src/calculation \ + --gnc-module-dir ${top_builddir}/src/gnome-utils \ + --gnc-module-dir ${top_srcdir}/src/gnc-module \ + --gnc-module-dir ${top_srcdir}/src/engine \ + --gnc-module-dir ${top_srcdir}/src/app-utils \ + --gnc-module-dir ${top_srcdir}/src/gnome-utils \ + --gnc-module-dir ${top_builddir}/src/gnome-utils \ + --gnc-module-dir ${top_builddir}/src/network-utils \ + --gnc-module-dir ${top_builddir}/src/gnome \ + --library-dir ${G_WRAP_LIB_DIR} \ + --guile-load-dir ${G_WRAP_MODULE_DIR} \ + --guile-load-dir ${top_srcdir}/src/scm \ + --guile-load-dir ${top_srcdir}/src/import-export + +TESTS_ENVIRONMENT := \ + $(shell ${top_srcdir}/src/gnc-test-env --no-exports ${GNC_TEST_DEPS}) + +noinst_PROGRAMS = \ + test-link \ + test-import-parse diff --git a/src/import-export/test/test-import-parse.c b/src/import-export/test/test-import-parse.c new file mode 100644 index 0000000000..6f662241bd --- /dev/null +++ b/src/import-export/test/test-import-parse.c @@ -0,0 +1,163 @@ +/* + * test-import-parse.c -- Test the import-parse routines. + * + * Created by: Derek Atkins + * + */ + +#include +#include + +#include "gnc-module.h" +#include "import-parse.h" + +#include "test-stuff.h" + +typedef struct { int y; int m; int d; } my_ymd_t; + +/* make sure the numbers are the same in the two sets of lists */ +const char* period_numbers[] = { " $+2000.00", "-2.00", "1,182,183.1827", NULL }; +const char* comma_numbers[] = { " $2000,00", "-2,00", "1.182.183,1827", NULL }; +const char* period_numbers_ambig[] = { " -$1,000 ", "100.277", NULL }; +const char* comma_numbers_ambig[] = { " -$1.000 ", "100,277", NULL }; + +/* Make sure the strings and numbers match... */ +const char* dates_ymd[] = { "1999/12/31", "2001-6-17", "20020726", NULL }; +my_ymd_t dates_ymd_vals[] = { {1999,12,31}, {2001,6,17}, {2002,7,26}, {0,0,0} }; +const char* dates_ydm[] = { "1999/31/12", "2001-17-6", "20012311", NULL }; +my_ymd_t dates_ydm_vals[] = { {1999,12,31}, {2001,6,17}, {2001,11,23}, {0,0,0} }; +const char* dates_mdy[] = { "1/16/2001", "12-31-1999", "01171983", NULL }; +my_ymd_t dates_mdy_vals[] = { {2001,1,16}, {1999,12,31}, {1983,1,17}, {0,0,0} }; +const char* dates_dmy[] = { "16/1/2001", "31-12-1999", "17011976", NULL }; +my_ymd_t dates_dmy_vals[] = { {2001,1,16}, {1999,12,31}, {1976,1,17}, {0,0,0} }; + +const char* dates_yxx[] = { "99/1/6", "1999-12'10", "20010306", NULL }; +const char* dates_xxy[] = { "1/3/99", "12-10'1999", "03062001", NULL }; + +static void +run_check(GncImportFormat (*check_fcn)(const char*, GncImportFormat), + const char *numbers[], GncImportFormat formats, + const char* msg, GncImportFormat expected) +{ + while (*numbers) { + do_test(check_fcn(*numbers, formats) == expected, msg); + numbers++; + } +} + +static void +test_check_numeric(void) +{ + GncImportFormat fmts; + + fmts = GNCIF_NUM_PERIOD | GNCIF_NUM_COMMA | GNCIF_DATE_MDY; + + run_check(gnc_import_test_numeric, period_numbers, fmts, + "Period numbers", GNCIF_NUM_PERIOD); + run_check(gnc_import_test_numeric, comma_numbers, fmts, + "Comma numbers", GNCIF_NUM_COMMA); + + run_check(gnc_import_test_numeric, period_numbers_ambig, fmts, + "Ambiguous Period numbers", GNCIF_NUM_PERIOD | GNCIF_NUM_COMMA); + run_check(gnc_import_test_numeric, comma_numbers_ambig, fmts, + "Ambiguous Comma numbers", GNCIF_NUM_PERIOD | GNCIF_NUM_COMMA); +} + +static void +test_check_date(void) +{ + GncImportFormat fmts; + + fmts = GNCIF_DATE_DMY | GNCIF_DATE_MDY | GNCIF_DATE_YMD | GNCIF_DATE_YDM; + + run_check(gnc_import_test_date, dates_ymd, fmts, "y/m/d dates", GNCIF_DATE_YMD); + run_check(gnc_import_test_date, dates_ydm, fmts, "y/d/m dates", GNCIF_DATE_YDM); + run_check(gnc_import_test_date, dates_mdy, fmts, "m/d/y dates", GNCIF_DATE_MDY); + run_check(gnc_import_test_date, dates_dmy, fmts, "d/m/y dates", GNCIF_DATE_DMY); + + run_check(gnc_import_test_date, dates_yxx, fmts, "y/x/x dates", + GNCIF_DATE_YMD | GNCIF_DATE_YDM); + run_check(gnc_import_test_date, dates_xxy, fmts, "x/x/y dates", + GNCIF_DATE_DMY | GNCIF_DATE_MDY); +} + +static void +test_numbers(const char* pstr, const char* cstr) +{ + gnc_numeric pval, cval; + + do_test(gnc_import_parse_numeric(pstr, GNCIF_NUM_PERIOD, &pval), "Parsing Period"); + do_test(gnc_import_parse_numeric(cstr, GNCIF_NUM_COMMA, &cval), "Parsing Comma"); + + do_test(gnc_numeric_equal(pval, cval), "Numbers equal?"); +} + +static void +test_number_strings(const char** pstrs, const char** cstrs) +{ + while (*pstrs && *cstrs) { + test_numbers(*pstrs, *cstrs); + pstrs++; + cstrs++; + } +} + +static void +test_parse_numeric(void) +{ + test_number_strings(period_numbers, comma_numbers); + test_number_strings(period_numbers_ambig, comma_numbers_ambig); +} + +static void +test_date(const char* str, GncImportFormat fmt, my_ymd_t date) +{ + Timespec ts, ts2;; + + do_test(gnc_import_parse_date(str, fmt, &ts), "Parsing date"); + ts2 = gnc_dmy2timespec(date.d, date.m, date.y); + do_test(timespec_equal(&ts, &ts2), "Date Equal"); +} + +static void +test_date_list(const char** strs, GncImportFormat fmt, my_ymd_t *dates) +{ + while (*strs && (*dates).y) { + test_date(*strs, fmt, *dates); + strs++; dates++; + } +} + +static void +test_parse_date(void) +{ + test_date_list(dates_ymd, GNCIF_DATE_YMD, dates_ymd_vals); + test_date_list(dates_ydm, GNCIF_DATE_YDM, dates_ydm_vals); + test_date_list(dates_mdy, GNCIF_DATE_MDY, dates_mdy_vals); + test_date_list(dates_dmy, GNCIF_DATE_DMY, dates_dmy_vals); +} + +static void +test_import_parse(void) +{ + test_check_numeric(); + test_check_date(); + test_parse_numeric(); + test_parse_date(); +} + +static void +main_helper(void *closure, int argc, char **argv) +{ + gnc_module_load("gnucash/import-export", 0); + test_import_parse(); + print_test_results(); + exit(get_rv()); +} + +int +main(int argc, char **argv) +{ + scm_boot_guile(argc, argv, main_helper, NULL); + return 0; +}