unofficial mirror of notmuch@notmuchmail.org
 help / color / mirror / code / Atom feed
* [PATCH v2 0/7] notmuch search date:since..until query support
@ 2012-08-04  7:41 Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 1/7] build: drop the -Wswitch-enum warning Jani Nikula
                   ` (6 more replies)
  0 siblings, 7 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

Hi all, immediate v2 of id:"cover.1344028781.git.jani@nikula.org" just
to rebase patch 3 against master. No other changes.

BR,
Jani.


Jani Nikula (7):
  build: drop the -Wswitch-enum warning
  lib: add a date/time parser module
  test: add new test tool parse-time for date/time parser
  test: add smoke tests for the date/time parser module
  lib: add date range query support
  test: add tests for date:since..until range queries
  man: document the date:since..until range queries

 configure                       |    2 +-
 lib/Makefile.local              |    2 +
 lib/database-private.h          |    1 +
 lib/database.cc                 |    5 +
 lib/parse-time-string.c         | 1384 +++++++++++++++++++++++++++++++++++++++
 lib/parse-time-string.h         |   95 +++
 lib/parse-time-vrp.cc           |   40 ++
 lib/parse-time-vrp.h            |   19 +
 man/man7/notmuch-search-terms.7 |  147 ++++-
 test/Makefile.local             |    7 +-
 test/basic                      |    2 +-
 test/notmuch-test               |    2 +
 test/parse-time-string          |   26 +
 test/parse-time.c               |  145 ++++
 test/search-date                |   21 +
 15 files changed, 1883 insertions(+), 15 deletions(-)
 create mode 100644 lib/parse-time-string.c
 create mode 100644 lib/parse-time-string.h
 create mode 100644 lib/parse-time-vrp.cc
 create mode 100644 lib/parse-time-vrp.h
 create mode 100755 test/parse-time-string
 create mode 100644 test/parse-time.c
 create mode 100755 test/search-date

-- 
1.7.9.5

^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 1/7] build: drop the -Wswitch-enum warning
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 2/7] lib: add a date/time parser module Jani Nikula
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

-Wswitch-enum is a bit awkward if a switch statement is intended to
handle just some of the named codes of an enumeration especially, and
leave the rest to the default label.

We already have -Wall, which enables -Wswitch by default, and per GCC
documentation, "The only difference between -Wswitch and this option
[-Wswitch-enum] is that this option gives a warning about an omitted
enumeration code even if there is a default label."

Drop -Wswitch-enum to not force listing all named codes of
enumerations in switch statements that have a default label.

---

This will be useful in the next patch.
---
 configure |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/configure b/configure
index dc0dba4..4433f42 100755
--- a/configure
+++ b/configure
@@ -528,7 +528,7 @@ fi
 
 WARN_CXXFLAGS=""
 printf "Checking for available C++ compiler warning flags... "
-for flag in -Wall -Wextra -Wwrite-strings -Wswitch-enum; do
+for flag in -Wall -Wextra -Wwrite-strings; do
     if ${CC} $flag -o minimal minimal.c > /dev/null 2>&1
     then
 	WARN_CXXFLAGS="${WARN_CXXFLAGS}${WARN_CXXFLAGS:+ }${flag}"
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 2/7] lib: add a date/time parser module
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 1/7] build: drop the -Wswitch-enum warning Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  2012-08-05 13:08   ` David Bremner
  2012-08-04  7:41 ` [PATCH v2 3/7] test: add new test tool parse-time for date/time parser Jani Nikula
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

Build a date/time parser as part of the notmuch lib, to be used for
adding date range query support later on.

Signed-off-by: Jani Nikula <jani@nikula.org>
---
 lib/Makefile.local      |    1 +
 lib/parse-time-string.c | 1384 +++++++++++++++++++++++++++++++++++++++++++++++
 lib/parse-time-string.h |   95 ++++
 3 files changed, 1480 insertions(+)
 create mode 100644 lib/parse-time-string.c
 create mode 100644 lib/parse-time-string.h

diff --git a/lib/Makefile.local b/lib/Makefile.local
index 8a9aa28..e29c3a2 100644
--- a/lib/Makefile.local
+++ b/lib/Makefile.local
@@ -53,6 +53,7 @@ libnotmuch_c_srcs =		\
 	$(dir)/libsha1.c	\
 	$(dir)/message-file.c	\
 	$(dir)/messages.c	\
+	$(dir)/parse-time-string.c	\
 	$(dir)/sha1.c		\
 	$(dir)/tags.c
 
diff --git a/lib/parse-time-string.c b/lib/parse-time-string.c
new file mode 100644
index 0000000..7c50f3e
--- /dev/null
+++ b/lib/parse-time-string.c
@@ -0,0 +1,1384 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include "parse-time-string.h"
+
+#define unused(x) x __attribute__ ((unused))
+
+/* REVISIT: Redefine these to add i18n support. The keyword table uses
+ * N_() to mark strings to be translated; they are accessed
+ * dynamically using _(). */
+#define _(s) (s)	/* i18n: define as gettext (s) */
+#define N_(s) (s)	/* i18n: define as gettext_noop (s) */
+
+#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
+
+/* field indices in struct state tm, and set fields */
+enum field {
+    /* keep SEC...YEAR in this order */
+    TM_ABS_SEC,		/* seconds */
+    TM_ABS_MIN,		/* minutes */
+    TM_ABS_HOUR,	/* hours */
+    TM_ABS_MDAY,	/* day of the month */
+    TM_ABS_MON,		/* month */
+    TM_ABS_YEAR,	/* year */
+
+    TM_ABS_WDAY,	/* day of the week. special: may be relative */
+    TM_ABS_ISDST,	/* daylight saving time */
+
+    TM_AMPM,		/* am vs. pm */
+    TM_TZ,		/* timezone in minutes */
+
+    /* keep SEC...YEAR in this order */
+    TM_REL_SEC,		/* seconds relative to now */
+    TM_REL_MIN,		/* minutes ... */
+    TM_REL_HOUR,	/* hours ... */
+    TM_REL_DAY,		/* days ... */
+    TM_REL_MON,		/* months ... */
+    TM_REL_YEAR,	/* years ... */
+    TM_REL_WEEK,	/* weeks ... */
+
+    TM_NONE,		/* not a field */
+
+    TM_SIZE = TM_NONE,
+};
+
+enum field_set {
+    FIELD_UNSET,
+    FIELD_SET,
+    FIELD_NOW,
+};
+
+static enum field
+next_field (enum field field)
+{
+    /* note: depends on the enum ordering */
+    return field < TM_ABS_YEAR ? field + 1 : TM_NONE;
+}
+
+static enum field
+abs_to_rel_field (enum field field)
+{
+    assert (field <= TM_ABS_YEAR);
+
+    /* note: depends on the enum ordering */
+    return field + (TM_REL_SEC - TM_ABS_SEC);
+}
+
+/* get zero value for field */
+static int
+field_zero (enum field field)
+{
+    if (field == TM_ABS_MDAY || field == TM_ABS_MON)
+	return 1;
+    else if (field == TM_ABS_YEAR)
+	return 1970;
+    else
+	return 0;
+}
+
+struct state {
+    int tm[TM_SIZE];			/* parsed date and time */
+    enum field_set set[TM_SIZE];	/* set status of tm */
+
+    enum field last_field;
+    char delim;
+
+    int postponed_length;	/* number of digits in postponed value */
+    int postponed_value;
+    char postponed_delim;
+};
+
+/*
+ * Helpers for postponed numbers.
+ *
+ * postponed_length is the number of digits in postponed value. 0
+ * means there is no postponed number. -1 means there is a postponed
+ * number, but it comes from a keyword, and it doesn't have digits.
+ */
+static int
+get_postponed_length (struct state *state)
+{
+    return state->postponed_length;
+}
+
+static bool
+get_postponed_number (struct state *state, int *v, int *n, char *d)
+{
+    if (!state->postponed_length)
+	return false;
+
+    if (n)
+	*n = state->postponed_length;
+
+    if (v)
+	*v = state->postponed_value;
+
+    if (d)
+	*d = state->postponed_delim;
+
+    state->postponed_length = 0;
+    state->postponed_value = 0;
+    state->postponed_delim = 0;
+
+    return true;
+}
+
+/* parse postponed number if one exists */
+static int parse_postponed_number (struct state *state, int v, int n, char d);
+static int
+handle_postponed_number (struct state *state)
+{
+    int v = state->postponed_value;
+    int n = state->postponed_length;
+    char d = state->postponed_delim;
+
+    if (!n)
+	return 0;
+
+    state->postponed_value = 0;
+    state->postponed_length = 0;
+    state->postponed_delim = 0;
+
+    return parse_postponed_number (state, v, n, d);
+}
+
+/*
+ * set new postponed number to be handled later. if one exists
+ * already, handle it first. n may be -1 to indicate a keyword that
+ * has no number length.
+ */
+static int
+set_postponed_number (struct state *state, int v, int n)
+{
+    int r;
+    char d = state->delim;
+
+    /* parse previous postponed number, if any */
+    r = handle_postponed_number (state);
+    if (r)
+	return r;
+
+    state->postponed_length = n;
+    state->postponed_value = v;
+    state->postponed_delim = d;
+
+    return 0;
+}
+
+static void
+set_delim (struct state *state, char delim)
+{
+    state->delim = delim;
+}
+
+static void
+unset_delim (struct state *state)
+{
+    state->delim = 0;
+}
+
+/*
+ * Field set/get/mod helpers.
+ */
+
+/* returns unset for non-tracked fields */
+static bool
+is_field_set (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    return field < ARRAY_SIZE (state->set) &&
+	   state->set[field] != FIELD_UNSET;
+}
+
+static void
+unset_field (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    state->set[field] = FIELD_UNSET;
+    state->tm[field] = 0;
+}
+
+/* Set field to value. */
+static int
+set_field (struct state *state, enum field field, int value)
+{
+    int r;
+
+    assert (field < ARRAY_SIZE (state->tm));
+
+    /* some fields can only be set once */
+    if (field < ARRAY_SIZE (state->set) && state->set[field] != FIELD_UNSET)
+	return -PARSE_TIME_ERR_ALREADYSET;
+
+    state->set[field] = FIELD_SET;
+
+    /*
+     * REVISIT: There could be a "next_field" that would be set from
+     * "field" for the duration of the handle_postponed_number() call,
+     * so it has more information to work with.
+     */
+
+    /* parse postponed number, if any */
+    r = handle_postponed_number (state);
+    if (r)
+	return r;
+
+    unset_delim (state);
+
+    state->tm[field] = value;
+    state->last_field = field;
+
+    return 0;
+}
+
+/*
+ * Mark n fields in fields to be set to current date/time in the
+ * specified time zone, or local timezone if not specified. The fields
+ * will be initialized after parsing is complete and timezone is
+ * known.
+ */
+static int
+set_fields_to_now (struct state *state, enum field *fields, size_t n)
+{
+    size_t i;
+    int r;
+
+    for (i = 0; i < n; i++) {
+	r = set_field (state, fields[i], 0);
+	if (r)
+	    return r;
+	state->set[fields[i]] = FIELD_NOW;
+    }
+
+    return 0;
+}
+
+/* Modify field by adding value to it. To be used on relative fields. */
+static int
+mod_field (struct state *state, enum field field, int value)
+{
+    int r;
+
+    assert (field < ARRAY_SIZE (state->tm));   /* assert relative??? */
+
+    if (field < ARRAY_SIZE (state->set))
+	state->set[field] = FIELD_SET;
+
+    /* parse postponed number, if any */
+    r = handle_postponed_number (state);
+    if (r)
+	return r;
+
+    unset_delim (state);
+
+    state->tm[field] += value;
+    state->last_field = field;
+
+    return 0;
+}
+
+/*
+ * Get field value. Make sure the field is set before query. It's most
+ * likely an error to call this while parsing (for example fields set
+ * as FIELD_NOW will only be set to some value after parsing).
+ */
+static int
+get_field (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    return state->tm[field];
+}
+
+/*
+ * Validity checkers.
+ */
+static bool is_valid_12hour (int h)
+{
+    return h >= 0 && h <= 12;
+}
+
+static bool is_valid_time (int h, int m, int s)
+{
+    /* allow 24:00:00 to denote end of day */
+    if (h == 24 && m == 0 && s == 0)
+	return true;
+
+    return h >= 0 && h <= 23 && m >= 0 && m <= 59 && s >= 0 && s <= 59;
+}
+
+static bool is_valid_mday (int mday)
+{
+    return mday >= 1 && mday <= 31;
+}
+
+static bool is_valid_mon (int mon)
+{
+    return mon >= 1 && mon <= 12;
+}
+
+static bool is_valid_year (int year)
+{
+    return year >= 1970;
+}
+
+static bool is_valid_date (int year, int mon, int mday)
+{
+    return is_valid_year (year) && is_valid_mon (mon) && is_valid_mday (mday);
+}
+
+/* Unset indicator for time and date set helpers. */
+#define UNSET -1
+
+/* Time set helper. No input checking. Use UNSET (-1) to leave unset. */
+static int
+set_abs_time (struct state *state, int hour, int min, int sec)
+{
+    int r;
+
+    if (hour != UNSET) {
+	if ((r = set_field (state, TM_ABS_HOUR, hour)))
+	    return r;
+    }
+
+    if (min != UNSET) {
+	if ((r = set_field (state, TM_ABS_MIN, min)))
+	    return r;
+    }
+
+    if (sec != UNSET) {
+	if ((r = set_field (state, TM_ABS_SEC, sec)))
+	    return r;
+    }
+
+    return 0;
+}
+
+/* Date set helper. No input checking. Use UNSET (-1) to leave unset. */
+static int
+set_abs_date (struct state *state, int year, int mon, int mday)
+{
+    int r;
+
+    if (year != UNSET) {
+	if ((r = set_field (state, TM_ABS_YEAR, year)))
+	    return r;
+    }
+
+    if (mon != UNSET) {
+	if ((r = set_field (state, TM_ABS_MON, mon)))
+	    return r;
+    }
+
+    if (mday != UNSET) {
+	if ((r = set_field (state, TM_ABS_MDAY, mday)))
+	    return r;
+    }
+
+    return 0;
+}
+
+/*
+ * Keyword parsing and handling.
+ */
+struct keyword;
+typedef int (*setter_t)(struct state *state, struct keyword *kw);
+
+struct keyword {
+    const char *name;	/* keyword */
+    enum field field;	/* field to set, or FIELD_NONE if N/A */
+    int value;		/* value to set, or 0 if N/A */
+    setter_t set;	/* function to use for setting, if non-NULL */
+};
+
+/*
+ * Setter callback functions for keywords.
+ */
+static int
+kw_set_default (struct state *state, struct keyword *kw)
+{
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_rel (struct state *state, struct keyword *kw)
+{
+    int multiplier = 1;
+
+    /* get a previously set multiplier, if any */
+    get_postponed_number (state, &multiplier, NULL, NULL);
+
+    /* accumulate relative field values */
+    return mod_field (state, kw->field, multiplier * kw->value);
+}
+
+static int
+kw_set_number (struct state *state, struct keyword *kw)
+{
+    /* -1 = no length, from keyword */
+    return set_postponed_number (state, kw->value, -1);
+}
+
+static int
+kw_set_month (struct state *state, struct keyword *kw)
+{
+    int n = get_postponed_length (state);
+
+    /* consume postponed number if it could be mday */
+    if (n == 1 || n == 2) {
+	int r, v;
+
+	get_postponed_number (state, &v, NULL, NULL);
+
+	if (!is_valid_mday (v))
+	    return -PARSE_TIME_ERR_INVALIDDATE;
+
+	r = set_field (state, TM_ABS_MDAY, v);
+	if (r)
+	    return r;
+    }
+
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_ampm (struct state *state, struct keyword *kw)
+{
+    int n = get_postponed_length (state);
+
+    /* consume postponed number if it could be hour */
+    if (n == 1 || n == 2) {
+	int r, v;
+
+	get_postponed_number (state, &v, NULL, NULL);
+
+	if (!is_valid_12hour (v))
+	    return -PARSE_TIME_ERR_INVALIDTIME;
+
+	r = set_abs_time (state, v, 0, 0);
+	if (r)
+	    return r;
+    }
+
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_timeofday (struct state *state, struct keyword *kw)
+{
+    return set_abs_time (state, kw->value, 0, 0);
+}
+
+static int
+kw_set_today (struct state *state, unused (struct keyword *kw))
+{
+    enum field fields[] = { TM_ABS_YEAR, TM_ABS_MON, TM_ABS_MDAY };
+
+    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));
+}
+
+static int
+kw_set_now (struct state *state, unused (struct keyword *kw))
+{
+    enum field fields[] = { TM_ABS_HOUR, TM_ABS_MIN, TM_ABS_SEC };
+
+    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));
+}
+
+static int
+kw_set_ordinal (struct state *state, struct keyword *kw)
+{
+    int n, v;
+
+    /* require a postponed number */
+    if (!get_postponed_number (state, &v, &n, NULL))
+	return -PARSE_TIME_ERR_DATEFORMAT;
+
+    /* ordinals are mday */
+    if (n != 1 && n != 2)
+	return -PARSE_TIME_ERR_DATEFORMAT;
+
+    /* be strict about st, nd, rd, and lax about th */
+    if (strcasecmp (kw->name, "st") == 0 && v != 1 && v != 21 && v != 31)
+	return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "nd") == 0 && v != 2 && v != 22)
+	return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "rd") == 0 && v != 3 && v != 23)
+	return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "th") == 0 && !is_valid_mday (v))
+	return -PARSE_TIME_ERR_INVALIDDATE;
+
+    return set_field (state, TM_ABS_MDAY, v);
+}
+
+/*
+ * Accepted keywords.
+ *
+ * A keyword may optionally contain a '|' to indicate the minimum
+ * match length. Without one, full match is required. It's advisable
+ * to keep the minimum match parts unique across all keywords.
+ *
+ * If keyword begins with upper case letter, then the matching will be
+ * case sensitive. Otherwise the matching is case insensitive.
+ *
+ * If setter is NULL, set_default will be used.
+ *
+ * Note: Order matters. Matching is greedy, longest match is used, but
+ * of equal length matches the first one is used.
+ */
+static struct keyword keywords[] = {
+    /* weekdays */
+    { N_("sun|day"),	TM_ABS_WDAY,	0,	NULL },
+    { N_("mon|day"),	TM_ABS_WDAY,	1,	NULL },
+    { N_("tue|sday"),	TM_ABS_WDAY,	2,	NULL },
+    { N_("wed|nesday"),	TM_ABS_WDAY,	3,	NULL },
+    { N_("thu|rsday"),	TM_ABS_WDAY,	4,	NULL },
+    { N_("fri|day"),	TM_ABS_WDAY,	5,	NULL },
+    { N_("sat|urday"),	TM_ABS_WDAY,	6,	NULL },
+
+    /* months */
+    { N_("jan|uary"),	TM_ABS_MON,	1,	kw_set_month },
+    { N_("feb|ruary"),	TM_ABS_MON,	2,	kw_set_month },
+    { N_("mar|ch"),	TM_ABS_MON,	3,	kw_set_month },
+    { N_("apr|il"),	TM_ABS_MON,	4,	kw_set_month },
+    { N_("may"),	TM_ABS_MON,	5,	kw_set_month },
+    { N_("jun|e"),	TM_ABS_MON,	6,	kw_set_month },
+    { N_("jul|y"),	TM_ABS_MON,	7,	kw_set_month },
+    { N_("aug|ust"),	TM_ABS_MON,	8,	kw_set_month },
+    { N_("sep|tember"),	TM_ABS_MON,	9,	kw_set_month },
+    { N_("oct|ober"),	TM_ABS_MON,	10,	kw_set_month },
+    { N_("nov|ember"),	TM_ABS_MON,	11,	kw_set_month },
+    { N_("dec|ember"),	TM_ABS_MON,	12,	kw_set_month },
+
+    /* durations */
+    { N_("y|ears"),	TM_REL_YEAR,	1,	kw_set_rel },
+    { N_("w|eeks"),	TM_REL_WEEK,	1,	kw_set_rel },
+    { N_("d|ays"),	TM_REL_DAY,	1,	kw_set_rel },
+    { N_("h|ours"),	TM_REL_HOUR,	1,	kw_set_rel },
+    { N_("hr|s"),	TM_REL_HOUR,	1,	kw_set_rel },
+    { N_("m|inutes"),	TM_REL_MIN,	1,	kw_set_rel },
+    /* M=months, m=minutes */
+    { N_("M"),		TM_REL_MON,	1,	kw_set_rel },
+    { N_("mins"),	TM_REL_MIN,	1,	kw_set_rel },
+    { N_("mo|nths"),	TM_REL_MON,	1,	kw_set_rel },
+    { N_("s|econds"),	TM_REL_SEC,	1,	kw_set_rel },
+    { N_("secs"),	TM_REL_SEC,	1,	kw_set_rel },
+
+    /* numbers */
+    { N_("one"),	TM_NONE,	1,	kw_set_number },
+    { N_("two"),	TM_NONE,	2,	kw_set_number },
+    { N_("three"),	TM_NONE,	3,	kw_set_number },
+    { N_("four"),	TM_NONE,	4,	kw_set_number },
+    { N_("five"),	TM_NONE,	5,	kw_set_number },
+    { N_("six"),	TM_NONE,	6,	kw_set_number },
+    { N_("seven"),	TM_NONE,	7,	kw_set_number },
+    { N_("eight"),	TM_NONE,	8,	kw_set_number },
+    { N_("nine"),	TM_NONE,	9,	kw_set_number },
+    { N_("ten"),	TM_NONE,	10,	kw_set_number },
+    { N_("dozen"),	TM_NONE,	12,	kw_set_number },
+    { N_("hundred"),	TM_NONE,	100,	kw_set_number },
+
+    /* special number forms */
+    { N_("this"),	TM_NONE,	0,	kw_set_number },
+    { N_("last"),	TM_NONE,	1,	kw_set_number },
+
+    /* specials */
+    { N_("yesterday"),	TM_REL_DAY,	1,	kw_set_rel },
+    { N_("today"),	TM_NONE,	0,	kw_set_today },
+    { N_("now"),	TM_NONE,	0,	kw_set_now },
+    { N_("noon"),	TM_NONE,	12,	kw_set_timeofday },
+    { N_("midnight"),	TM_NONE,	0,	kw_set_timeofday },
+    { N_("am"),		TM_AMPM,	0,	kw_set_ampm },
+    { N_("a.m."),	TM_AMPM,	0,	kw_set_ampm },
+    { N_("pm"),		TM_AMPM,	1,	kw_set_ampm },
+    { N_("p.m."),	TM_AMPM,	1,	kw_set_ampm },
+    { N_("st"),		TM_NONE,	0,	kw_set_ordinal },
+    { N_("nd"),		TM_NONE,	0,	kw_set_ordinal },
+    { N_("rd"),		TM_NONE,	0,	kw_set_ordinal },
+    { N_("th"),		TM_NONE,	0,	kw_set_ordinal },
+
+    /* timezone codes: offset in minutes. FIXME: add more codes. */
+    { N_("pst"),	TM_TZ,		-8*60,	NULL },
+    { N_("mst"),	TM_TZ,		-7*60,	NULL },
+    { N_("cst"),	TM_TZ,		-6*60,	NULL },
+    { N_("est"),	TM_TZ,		-5*60,	NULL },
+    { N_("ast"),	TM_TZ,		-4*60,	NULL },
+    { N_("nst"),	TM_TZ,		-(3*60+30),	NULL },
+
+    { N_("gmt"),	TM_TZ,		0,	NULL },
+    { N_("utc"),	TM_TZ,		0,	NULL },
+
+    { N_("wet"),	TM_TZ,		0,	NULL },
+    { N_("cet"),	TM_TZ,		1*60,	NULL },
+    { N_("eet"),	TM_TZ,		2*60,	NULL },
+    { N_("fet"),	TM_TZ,		3*60,	NULL },
+
+    { N_("wat"),	TM_TZ,		1*60,	NULL },
+    { N_("cat"),	TM_TZ,		2*60,	NULL },
+    { N_("eat"),	TM_TZ,		3*60,	NULL },
+};
+
+/*
+ * Compare strings s and keyword. Return number of matching chars on
+ * match, 0 for no match. Match must be at least n chars (n == 0 all
+ * of keyword), otherwise it's not a match. Use match_case for case
+ * sensitive matching.
+ */
+static size_t
+stringcmp (const char *s, const char *keyword, size_t n, bool match_case)
+{
+    size_t i;
+
+    for (i = 0; *s && *keyword; i++, s++, keyword++) {
+	if (match_case) {
+	    if (*s != *keyword)
+		break;
+	} else {
+	    if (tolower ((unsigned char) *s) !=
+		tolower ((unsigned char) *keyword))
+		break;
+	}
+    }
+
+    if (n)
+	return i < n ? 0 : i;
+    else
+	return *keyword ? 0 : i;
+}
+
+/*
+ * Parse a keyword. Return < 0 on error, number of parsed chars on
+ * success.
+ */
+static ssize_t
+parse_keyword (struct state *state, const char *s)
+{
+    unsigned int i;
+    size_t n, max_n = 0;
+    struct keyword *kw = NULL;
+    int r;
+
+    /* Match longest keyword */
+    for (i = 0; i < ARRAY_SIZE (keywords); i++) {
+	/* Match case if keyword begins with upper case letter. */
+	bool mcase = isupper ((unsigned char) keywords[i].name[0]);
+	size_t minlen = 0;
+	char keyword[128];
+	char *p;
+
+	strncpy (keyword, _(keywords[i].name), sizeof (keyword));
+
+	/* Truncate too long keywords. REVISIT: Make this dynamic? */
+	keyword[sizeof (keyword) - 1] = '\0';
+
+	/* Minimum match length. */
+	p = strchr (keyword, '|');
+	if (p) {
+	    minlen = p - keyword;
+	    memmove (p, p + 1, strlen (p + 1) + 1);
+	}
+
+	n = stringcmp (s, keyword, minlen, mcase);
+	if (n > max_n || (n == max_n && mcase)) {
+	    max_n = n;
+	    kw = &keywords[i];
+	}
+    }
+
+    if (!kw)
+	return -PARSE_TIME_ERR_KEYWORD;
+
+    if (kw->set)
+	r = kw->set (state, kw);
+    else
+	r = kw_set_default (state, kw);
+
+    if (r < 0)
+	return r;
+
+    return max_n;
+}
+
+/*
+ * Non-keyword parsers and their helpers.
+ */
+
+static int
+set_user_tz (struct state *state, char sign, int hour, int min)
+{
+    int tz = hour * 60 + min;
+
+    assert (sign == '+' || sign == '-');
+
+    if (hour < 0 || hour > 14 || min < 0 || min > 59 || min % 15)
+	return -PARSE_TIME_ERR_INVALIDTIME;
+
+    if (sign == '-')
+	tz = -tz;
+
+    return set_field (state, TM_TZ, tz);
+}
+
+/*
+ * Independent parsing of a postponed number when it wasn't consumed
+ * during parsing of the following token.
+ *
+ * This should be able to trust that last_field and next_field are
+ * right.
+ */
+static int
+parse_postponed_number (struct state *state, int v, int n, char d)
+{
+    /*
+     * alright, these are really lone, won't affect parsing of
+     * following items... it's not a multiplier, those have been eaten
+     * away.
+     *
+     * also note numbers eaten away by parse_single_number.
+     */
+
+    assert (n < 8);
+
+    if (n == 1 || n == 2) {
+	if (state->last_field == TM_ABS_MON) {
+	    /* D[D] */
+	    if (!is_valid_mday (v))
+		return -PARSE_TIME_ERR_INVALIDDATE;
+
+	    return set_field (state, TM_ABS_MDAY, v);
+	} else if (n == 2) {
+	    /* REVISIT: only allow if last field is hour, min, or sec? */
+	    if (d == '+' || d == '-') {
+		/* +/-HH */
+		return set_user_tz (state, d, v, 0);
+	    }
+	}
+    } else if (n == 4) {
+	/* Notable exception: Value affects parsing. */
+	if (!is_valid_year (v)) {
+	    if (d == '+' || d == '-') {
+		/* +/-HHMM */
+		return set_user_tz (state, d, v / 100, v % 100);
+	    }
+	} else {
+	    /* YYYY */
+	    return set_field (state, TM_ABS_YEAR, v);
+	}
+    } else if (n == 6) {
+	/* HHMMSS */
+	int hour = v / 10000;
+	int min = (v / 100) % 100;
+	int sec = v % 100;
+
+	if (!is_valid_time (hour, min, sec))
+	    return -PARSE_TIME_ERR_INVALIDTIME;
+
+	return set_abs_time (state, hour, min, sec);
+    }
+
+    /* else n is one of {-1, 3, 5, 7 } */
+
+    return -PARSE_TIME_ERR_FORMAT;
+}
+
+/* Parse a single number. Typically postpone parsing until later. */
+static int
+parse_single_number (struct state *state, unsigned long v,
+		     unsigned long n)
+{
+    assert (n);
+
+    /* parse things that can be parsed immediately */
+    if (n == 8) {
+	/* YYYYMMDD */
+	int year = v / 10000;
+	int mon = (v / 100) % 100;
+	int mday = v % 100;
+
+	if (!is_valid_date (year, mon, mday))
+	    return -PARSE_TIME_ERR_INVALIDDATE;
+
+	return set_abs_date (state, year, mon, mday);
+    } else if (n > 8) {
+	/* FIXME: seconds since epoch */
+	return -PARSE_TIME_ERR_FORMAT;
+    }
+
+    if (v > INT_MAX)
+	return -PARSE_TIME_ERR_FORMAT;
+
+    return set_postponed_number (state, v, n);
+}
+
+static bool
+is_time_sep (char c)
+{
+    return c == ':';
+}
+
+static bool
+is_date_sep (char c)
+{
+    return c == '/' || c == '-' || c == '.';
+}
+
+static bool
+is_sep (char c)
+{
+    return is_time_sep (c) || is_date_sep (c);
+}
+
+/* two-digit year: 00...69 is 2000s, 70...99 1900s, if n == 0 keep unset */
+static int
+expand_year (unsigned long year, size_t n)
+{
+    if (n == 2) {
+	return (year < 70 ? 2000 : 1900) + year;
+    } else if (n == 4) {
+	return year;
+    } else {
+	return UNSET;
+    }
+}
+
+static int
+parse_date (struct state *state, char sep,
+	    unsigned long v1, unsigned long v2, unsigned long v3,
+	    size_t n1, size_t n2, size_t n3)
+{
+    int year = UNSET, mon = UNSET, mday = UNSET;
+
+    assert (is_date_sep (sep));
+
+    switch (sep) {
+    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */
+	if (n1 != 1 && n1 != 2)
+	    return -PARSE_TIME_ERR_DATEFORMAT;
+
+	if ((n2 == 1 || n2 == 2) && (n3 == 0 || n3 == 2 || n3 == 4)) {
+	    /* M[M]/D[D][/YY[YY]] */
+	    year = expand_year (v3, n3);
+	    mon = v1;
+	    mday = v2;
+	} else if (n2 == 4 && n3 == 0) {
+	    /* M[M]/YYYY */
+	    year = v2;
+	    mon = v1;
+	} else {
+	    return -PARSE_TIME_ERR_DATEFORMAT;
+	}
+	break;
+
+    case '-': /* Date: YYYY-MM[-DD] or DD-MM[-YY[YY]] or MM-YYYY */
+	if (n1 == 4 && n2 == 2 && (n3 == 0 || n3 == 2)) {
+	    /* YYYY-MM[-DD] */
+	    year = v1;
+	    mon = v2;
+	    if (n3)
+		mday = v3;
+	} else if (n1 == 2 && n2 == 2 && (n3 == 0 || n3 == 2 || n3 == 4)) {
+	    /* DD-MM[-YY[YY]] */
+	    year = expand_year (v3, n3);
+	    mon = v2;
+	    mday = v1;
+	} else if (n1 == 2 && n2 == 4 && n3 == 0) {
+	    /* MM-YYYY */
+	    year = v2;
+	    mon = v1;
+	} else {
+	    return -PARSE_TIME_ERR_DATEFORMAT;
+	}
+	break;
+
+    case '.': /* Date: D[D].M[M][.[YY[YY]]] */
+	if ((n1 != 1 && n1 != 2) || (n2 != 1 && n2 != 2) ||
+	    (n3 != 0 && n3 != 2 && n3 != 4))
+	    return -PARSE_TIME_ERR_DATEFORMAT;
+
+	year = expand_year (v3, n3);
+	mon = v2;
+	mday = v1;
+	break;
+    }
+
+    if (year != UNSET && !is_valid_year (year))
+	return -PARSE_TIME_ERR_INVALIDDATE;
+
+    if (mon != UNSET && !is_valid_mon (mon))
+	return -PARSE_TIME_ERR_INVALIDDATE;
+
+    if (mday != UNSET && !is_valid_mday (mday))
+	return -PARSE_TIME_ERR_INVALIDDATE;
+
+    return set_abs_date (state, year, mon, mday);
+}
+
+static int
+parse_time (struct state *state, char sep,
+	    unsigned long v1, unsigned long v2, unsigned long v3,
+	    size_t n1, size_t n2, size_t n3)
+{
+    assert (is_time_sep (sep));
+
+    if ((n1 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 2))
+	return -PARSE_TIME_ERR_TIMEFORMAT;
+
+    /*
+     * REVISIT: this means it's required to set time *before* being
+     * able to set timezone
+     */
+    if (is_field_set (state, TM_ABS_HOUR) &&
+	is_field_set (state, TM_ABS_MIN) &&
+	n1 == 2 && n2 == 2 && n3 == 0 &&
+	(state->delim == '+' || state->delim == '-')) {
+	return set_user_tz (state, state->delim, v1, v2);
+    }
+
+    if (!is_valid_time (v1, v2, v3))
+	return -PARSE_TIME_ERR_INVALIDTIME;
+
+    return set_abs_time (state, v1, v2, n3 ? v3 : 0);
+}
+
+/* strtoul helper that assigns length */
+static unsigned long
+strtoul_len (const char *s, const char **endp, size_t *len)
+{
+    unsigned long val = strtoul (s, (char **) endp, 10);
+
+    *len = *endp - s;
+    return val;
+}
+
+/*
+ * Parse a (group of) number(s). Return < 0 on error, number of parsed
+ * chars on success.
+ */
+static ssize_t
+parse_number (struct state *state, const char *s)
+{
+    int r;
+    unsigned long v1, v2, v3 = 0;
+    size_t n1, n2, n3 = 0;
+    const char *p = s;
+    char sep;
+
+    v1 = strtoul_len (p, &p, &n1);
+
+    if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {
+	sep = *p;
+	v2 = strtoul_len (p + 1, &p, &n2);
+    } else {
+	/* a single number */
+	r = parse_single_number (state, v1, n1);
+	if (r)
+	    return r;
+
+	return p - s;
+    }
+
+    /* a group of two or three numbers? */
+    if (*p == sep && isdigit ((unsigned char) *(p + 1)))
+	v3 = strtoul_len (p + 1, &p, &n3);
+
+    if (is_time_sep (sep))
+	r = parse_time (state, sep, v1, v2, v3, n1, n2, n3);
+    else
+	r = parse_date (state, sep, v1, v2, v3, n1, n2, n3);
+
+    if (r)
+	return r;
+
+    return p - s;
+}
+
+/*
+ * Parse delimiter(s). Return < 0 on error, number of parsed chars on
+ * success.
+ */
+static ssize_t
+parse_delim (struct state *state, const char *s)
+{
+    const char *p = s;
+
+    /*
+     * REVISIT: any actions depending on the first delim after last
+     * field? what could it be?
+     */
+
+    /*
+     * skip non-alpha and non-digit, and store the last for further
+     * processing
+     */
+    while (*p && !isalnum ((unsigned char) *p)) {
+	set_delim (state, *p);
+	p++;
+    }
+
+    return p - s;
+}
+
+/*
+ * Parse a date/time string. Return < 0 on error, number of parsed
+ * chars on success.
+ */
+static ssize_t
+parse_input (struct state *state, const char *s)
+{
+    const char *p = s;
+    ssize_t n;
+    int r;
+
+    while (*p) {
+	if (isalpha ((unsigned char) *p)) {
+	    n = parse_keyword (state, p);
+	} else if (isdigit ((unsigned char) *p)) {
+	    n = parse_number (state, p);
+	} else {
+	    n = parse_delim (state, p);
+	}
+
+	if (n <= 0) {
+	    if (n == 0)
+		n = -PARSE_TIME_ERR;
+
+	    return n;
+	}
+
+	p += n;
+    }
+
+    /* parse postponed number, if any */
+    r = handle_postponed_number (state);
+    if (r < 0)
+	return r;
+
+    return p - s;
+}
+
+/*
+ * Processing the parsed input.
+ */
+
+/*
+ * Initialize reference time to tm. Use time zone in state if
+ * specified, otherwise local time. Use now for reference time if
+ * non-NULL, otherwise current time.
+ */
+static int
+initialize_now (struct state *state, struct tm *tm, const time_t *now)
+{
+    time_t t;
+
+    if (now) {
+	t = *now;
+    } else {
+	if (time (&t) == (time_t) -1)
+	    return -PARSE_TIME_ERR_LIB;
+    }
+
+    if (is_field_set (state, TM_TZ)) {
+	/* some other time zone */
+
+	/* adjust now according to the TZ */
+	t += get_field (state, TM_TZ) * 60;
+
+	/* it's not gm, but this doesn't mess with the tz */
+	if (gmtime_r (&t, tm) == NULL)
+	    return -PARSE_TIME_ERR_LIB;
+    } else {
+	/* local time */
+	if (localtime_r (&t, tm) == NULL)
+	    return -PARSE_TIME_ERR_LIB;
+    }
+
+    return 0;
+}
+
+/*
+ * Normalize tm according to mktime(3). Both mktime(3) and
+ * localtime_r(3) use local time, but they cancel each other out here,
+ * making this function agnostic to time zone.
+ */
+static int
+normalize_tm (struct tm *tm)
+{
+    time_t t = mktime (tm);
+
+    if (t == (time_t) -1)
+	return -PARSE_TIME_ERR_LIB;
+
+    if (!localtime_r (&t, tm))
+	return -PARSE_TIME_ERR_LIB;
+
+    return 0;
+}
+
+/* Get field out of a struct tm. */
+static int
+tm_get_field (const struct tm *tm, enum field field)
+{
+    switch (field) {
+    case TM_ABS_SEC:	return tm->tm_sec;
+    case TM_ABS_MIN:	return tm->tm_min;
+    case TM_ABS_HOUR:	return tm->tm_hour;
+    case TM_ABS_MDAY:	return tm->tm_mday;
+    case TM_ABS_MON:	return tm->tm_mon + 1; /* 0- to 1-based */
+    case TM_ABS_YEAR:	return 1900 + tm->tm_year;
+    case TM_ABS_WDAY:	return tm->tm_wday;
+    case TM_ABS_ISDST:	return tm->tm_isdst;
+    default:
+	assert (false);
+	break;
+    }
+
+    return 0;
+}
+
+/* Modify hour according to am/pm setting. */
+static int
+fixup_ampm (struct state *state)
+{
+    int hour, hdiff = 0;
+
+    if (!is_field_set (state, TM_AMPM))
+	return 0;
+
+    if (!is_field_set (state, TM_ABS_HOUR))
+	return -PARSE_TIME_ERR_TIMEFORMAT;
+
+    hour = get_field (state, TM_ABS_HOUR);
+    if (!is_valid_12hour (hour))
+	return -PARSE_TIME_ERR_INVALIDTIME;
+
+    if (get_field (state, TM_AMPM)) {
+	/* 12pm is noon */
+	if (hour != 12)
+	    hdiff = 12;
+    } else {
+	/* 12am is midnight, beginning of day */
+	if (hour == 12)
+	    hdiff = -12;
+    }
+
+    mod_field (state, TM_REL_HOUR, -hdiff);
+
+    return 0;
+}
+
+/* Combine absolute and relative fields, and round. */
+static int
+create_output (struct state *state, time_t *t_out, const time_t *tnow,
+	       int round)
+{
+    struct tm tm = { .tm_isdst = -1 };
+    struct tm now;
+    time_t t;
+    enum field f;
+    int r;
+    int week_round = PARSE_TIME_NO_ROUND;
+
+    r = initialize_now (state, &now, tnow);
+    if (r)
+	return r;
+
+    /* initialize uninitialized fields to now */
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {
+	if (state->set[f] == FIELD_NOW) {
+	    state->tm[f] = tm_get_field (&now, f);
+	    state->set[f] = FIELD_SET;
+	}
+    }
+
+    /*
+     * If MON is set but YEAR is not, refer to past month.
+     *
+     * REVISIT: Why are month/week special in this regard? What about
+     * mday, or time. Should refer to past.
+     */
+    if (is_field_set (state, TM_ABS_MON) &&
+	!is_field_set (state, TM_ABS_YEAR)) {
+	if (get_field (state, TM_ABS_MON) >= tm_get_field (&now, TM_ABS_MON))
+	    mod_field (state, TM_REL_YEAR, 1);
+    }
+
+    /*
+     * If WDAY is set but MDAY is not, we consider WDAY relative
+     *
+     * REVISIT: This fails on stuff like "two months ago monday"
+     * because two months ago wasn't the same day as today. Postpone
+     * until we know date?
+     */
+    if (is_field_set (state, TM_ABS_WDAY) &&
+	!is_field_set (state, TM_ABS_MDAY)) {
+	int wday = get_field (state, TM_ABS_WDAY);
+	int today = tm_get_field (&now, TM_ABS_WDAY);
+	int rel_days;
+
+	if (today > wday)
+	    rel_days = today - wday;
+	else
+	    rel_days = today + 7 - wday;
+
+	/* this also prevents special week rounding from happening */
+	mod_field (state, TM_REL_DAY, rel_days);
+
+	unset_field (state, TM_ABS_WDAY);
+    }
+
+    r = fixup_ampm (state);
+    if (r)
+	return r;
+
+    /*
+     * Iterate fields from most accurate to least accurate, and set
+     * unset fields according to requested rounding.
+     */
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {
+	if (round != PARSE_TIME_NO_ROUND) {
+	    enum field r = abs_to_rel_field (f);
+
+	    if (is_field_set (state, f) || is_field_set (state, r)) {
+		if (round >= PARSE_TIME_ROUND_UP)
+		    mod_field (state, r, -1);
+		round = PARSE_TIME_NO_ROUND; /* no more rounding */
+	    } else {
+		if (f == TM_ABS_MDAY &&
+		    is_field_set (state, TM_REL_WEEK)) {
+		    /* week is most accurate */
+		    week_round = round;
+		    round = PARSE_TIME_NO_ROUND;
+		} else {
+		    set_field (state, f, field_zero (f));
+		}
+	    }
+	}
+
+	if (!is_field_set (state, f))
+	    set_field (state, f, tm_get_field (&now, f));
+    }
+
+    /* special case: rounding with week accuracy */
+    if (week_round != PARSE_TIME_NO_ROUND) {
+	/* temporarily set more accurate fields to now */
+	set_field (state, TM_ABS_SEC, tm_get_field (&now, TM_ABS_SEC));
+	set_field (state, TM_ABS_MIN, tm_get_field (&now, TM_ABS_MIN));
+	set_field (state, TM_ABS_HOUR, tm_get_field (&now, TM_ABS_HOUR));
+	set_field (state, TM_ABS_MDAY, tm_get_field (&now, TM_ABS_MDAY));
+    }
+
+    /*
+     * set all fields. they may contain out of range values before
+     * normalization by mktime(3).
+     */
+    tm.tm_sec = get_field (state, TM_ABS_SEC) - get_field (state, TM_REL_SEC);
+    tm.tm_min = get_field (state, TM_ABS_MIN) - get_field (state, TM_REL_MIN);
+    tm.tm_hour = get_field (state, TM_ABS_HOUR) - get_field (state, TM_REL_HOUR);
+    tm.tm_mday = get_field (state, TM_ABS_MDAY) -
+		 get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK);
+    tm.tm_mon = get_field (state, TM_ABS_MON) - get_field (state, TM_REL_MON);
+    tm.tm_mon--; /* 1- to 0-based */
+    tm.tm_year = get_field (state, TM_ABS_YEAR) - get_field (state, TM_REL_YEAR) - 1900;
+
+    /*
+     * It's always normal time.
+     *
+     * REVISIT: This is probably not a solution that universally
+     * works. Just make sure DST is not taken into account. We don't
+     * want rounding to be affected by DST.
+     */
+    tm.tm_isdst = -1;
+
+    /* special case: rounding with week accuracy */
+    if (week_round != PARSE_TIME_NO_ROUND) {
+	/* normalize to get proper tm.wday */
+	r = normalize_tm (&tm);
+	if (r < 0)
+	    return r;
+
+	/* set more accurate fields back to zero */
+	tm.tm_sec = 0;
+	tm.tm_min = 0;
+	tm.tm_hour = 0;
+	tm.tm_isdst = -1;
+
+	/* monday is the true 1st day of week, but this is easier */
+	if (week_round <= PARSE_TIME_ROUND_DOWN)
+	    tm.tm_mday -= tm.tm_wday;
+	else
+	    tm.tm_mday += 7 - tm.tm_wday;
+    }
+
+    if (is_field_set (state, TM_TZ)) {
+	/* tm is in specified TZ, convert to UTC for timegm(3) */
+	tm.tm_min -= get_field (state, TM_TZ);
+	t = timegm (&tm);
+    } else {
+	/* tm is in local time */
+	t = mktime (&tm);
+    }
+
+    if (t == (time_t) -1)
+	return -PARSE_TIME_ERR_LIB;
+
+    *t_out = t;
+
+    return 0;
+}
+
+/* internally, all errors are < 0. parse_time_string() returns errors > 0. */
+#define EXTERNAL_ERR(r) (-r)
+
+int
+parse_time_string (const char *s, time_t *t, const time_t *now, int round)
+{
+    struct state state = { .last_field = TM_NONE };
+    int r;
+
+    if (!s || !t)
+	return EXTERNAL_ERR (-PARSE_TIME_ERR);
+
+    r = parse_input (&state, s);
+    if (r < 0)
+	return EXTERNAL_ERR (r);
+
+    r = create_output (&state, t, now, round);
+    if (r < 0)
+	return EXTERNAL_ERR (r);
+
+    return 0;
+}
diff --git a/lib/parse-time-string.h b/lib/parse-time-string.h
new file mode 100644
index 0000000..50b7c6f
--- /dev/null
+++ b/lib/parse-time-string.h
@@ -0,0 +1,95 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#ifndef PARSE_TIME_STRING_H
+#define PARSE_TIME_STRING_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <time.h>
+
+/* return values for parse_time_string() */
+enum {
+    PARSE_TIME_OK = 0,
+    PARSE_TIME_ERR,		/* unspecified error */
+    PARSE_TIME_ERR_LIB,		/* library call failed */
+    PARSE_TIME_ERR_ALREADYSET,	/* attempt to set unit twice */
+    PARSE_TIME_ERR_FORMAT,	/* generic date/time format error */
+    PARSE_TIME_ERR_DATEFORMAT,	/* date format error */
+    PARSE_TIME_ERR_TIMEFORMAT,	/* time format error */
+    PARSE_TIME_ERR_INVALIDDATE,	/* date value error */
+    PARSE_TIME_ERR_INVALIDTIME,	/* time value error */
+    PARSE_TIME_ERR_KEYWORD,	/* unknown keyword */
+};
+
+/* round values for parse_time_string() */
+enum {
+    PARSE_TIME_ROUND_DOWN = -1,
+    PARSE_TIME_NO_ROUND = 0,
+    PARSE_TIME_ROUND_UP = 1,
+};
+
+/**
+ * parse_time_string() - user friendly date and time parser
+ * @s:		string to parse
+ * @t:		pointer to time_t to store parsed time in
+ * @now:	pointer to time_t containing reference date/time, or NULL
+ * @round:	PARSE_TIME_NO_ROUND, PARSE_TIME_ROUND_DOWN, or
+ *		PARSE_TIME_ROUND_UP
+ *
+ * Parse a date/time string 's' and store the parsed date/time result
+ * in 't'.
+ *
+ * A reference date/time is used for determining the "date/time units"
+ * (roughly equivalent to struct tm members) not specified by 's'. If
+ * 'now' is non-NULL, it must contain a pointer to a time_t to be used
+ * as reference date/time. Otherwise, the current time is used.
+ *
+ * If 's' does not specify a full date/time, the 'round' parameter
+ * specifies if and how the result should be rounded as follows:
+ *
+ *   PARSE_TIME_NO_ROUND: All date/time units that are not specified
+ *   by 's' are set to the corresponding unit derived from the
+ *   reference date/time.
+ *
+ *   PARSE_TIME_ROUND_DOWN: All date/time units that are more accurate
+ *   than the most accurate unit specified by 's' are set to the
+ *   smallest valid value for that unit. Rest of the unspecified units
+ *   are set as in PARSE_TIME_NO_ROUND.
+ *
+ *   PARSE_TIME_ROUND_UP: All date/time units that are more accurate
+ *   than the most accurate unit specified by 's' are set to the
+ *   smallest valid value for that unit. The most accurate unit
+ *   specified by 's' is incremented by one (and this is rolled over
+ *   to the less accurate units as necessary). Rest of the unspecified
+ *   units are set as in PARSE_TIME_NO_ROUND.
+ *
+ * Return 0 (PARSE_TIME_OK) for succesfully parsed date/time, or one
+ * of PARSE_TIME_ERR_* on error. 't' is not modified on error.
+ */
+int parse_time_string (const char *s, time_t *t, const time_t *now, int round);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PARSE_TIME_STRING_H */
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 3/7] test: add new test tool parse-time for date/time parser
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 1/7] build: drop the -Wswitch-enum warning Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 2/7] lib: add a date/time parser module Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 4/7] test: add smoke tests for the date/time parser module Jani Nikula
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

Add a tool to support testing the date/time parser module directly and
independent of the rest of notmuch.
---
 test/Makefile.local |    7 ++-
 test/basic          |    2 +-
 test/parse-time.c   |  145 +++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 152 insertions(+), 2 deletions(-)
 create mode 100644 test/parse-time.c

diff --git a/test/Makefile.local b/test/Makefile.local
index c7f1435..1a76617 100644
--- a/test/Makefile.local
+++ b/test/Makefile.local
@@ -19,9 +19,13 @@ $(dir)/smtp-dummy: $(smtp_dummy_modules)
 $(dir)/symbol-test: $(dir)/symbol-test.o
 	$(call quiet,CXX) $^ -o $@ -Llib -lnotmuch -lxapian
 
+$(dir)/parse-time: $(dir)/parse-time.o lib/parse-time-string.o
+	$(call quiet,CC) $^ -o $@
+
 .PHONY: test check
 
-test-binaries: $(dir)/arg-test $(dir)/smtp-dummy $(dir)/symbol-test
+test-binaries: $(dir)/arg-test $(dir)/smtp-dummy $(dir)/symbol-test \
+	$(dir)/parse-time
 
 test:	all test-binaries
 	@${dir}/notmuch-test $(OPTIONS)
@@ -32,4 +36,5 @@ SRCS := $(SRCS) $(smtp_dummy_srcs)
 CLEAN := $(CLEAN) $(dir)/smtp-dummy $(dir)/smtp-dummy.o \
 	 $(dir)/symbol-test $(dir)/symbol-test.o \
 	 $(dir)/arg-test $(dir)/arg-test.o \
+	 $(dir)/parse-time $(dir)/parse-time.o \
 	 $(dir)/corpus.mail $(dir)/test-results $(dir)/tmp.*
diff --git a/test/basic b/test/basic
index d6aed24..c658b95 100755
--- a/test/basic
+++ b/test/basic
@@ -54,7 +54,7 @@ test_begin_subtest 'Ensure that all available tests will be run by notmuch-test'
 eval $(sed -n -e '/^TESTS="$/,/^"$/p' $TEST_DIRECTORY/notmuch-test)
 tests_in_suite=$(for i in $TESTS; do echo $i; done | sort)
 available=$(find "$TEST_DIRECTORY" -maxdepth 1 -type f -executable -printf '%f\n' | \
-    sed -r -e "/^(aggregate-results.sh|notmuch-test|smtp-dummy|test-verbose|symbol-test|arg-test)$/d" | \
+    sed -r -e "/^(aggregate-results.sh|notmuch-test|smtp-dummy|test-verbose|symbol-test|arg-test|parse-time)$/d" | \
     sort)
 test_expect_equal "$tests_in_suite" "$available"
 
diff --git a/test/parse-time.c b/test/parse-time.c
new file mode 100644
index 0000000..b4de76b
--- /dev/null
+++ b/test/parse-time.c
@@ -0,0 +1,145 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "parse-time-string.h"
+
+/*
+ * concat argv[start]...argv[end - 1], separating them by a single
+ * space, to a malloced string
+ */
+static char *
+concat_args (int start, int end, char *argv[])
+{
+    int i;
+    size_t len = 1;
+    char *p;
+
+    for (i = start; i < end; i++)
+	len += strlen (argv[i]) + 1;
+
+    p = malloc (len);
+    if (!p)
+	return NULL;
+
+    *p = 0;
+
+    for (i = start; i < end; i++) {
+	if (i != start)
+	    strcat (p, " ");
+	strcat (p, argv[i]);
+    }
+
+    return p;
+}
+
+#define DEFAULT_FORMAT "%a %b %d %T %z %Y"
+
+static void
+usage (const char *name)
+{
+    printf ("Usage: %s [options ...] <date/time>\n\n", name);
+    printf (
+	"Parse <date/time> and display it in given format.\n\n"
+	"  -f, --format=FMT output format, FMT according to strftime(3)\n"
+	"                   (default: \"%s\")\n"
+	"  -n, --now=N      use N seconds since epoch as now (default: now)\n"
+	"  -u, --up         round result up (default: no rounding)\n"
+	"  -d, --down       round result down (default: no rounding)\n"
+	"  -h, --help       print this help\n",
+	DEFAULT_FORMAT);
+}
+
+int
+main (int argc, char *argv[])
+{
+    int r;
+    struct tm tm;
+    time_t result;
+    time_t now;
+    time_t *nowp = NULL;
+    char *argstr;
+    int round = PARSE_TIME_NO_ROUND;
+    char buf[1024];
+    const char *format = DEFAULT_FORMAT;
+    struct option options[] = {
+	{ "help",	no_argument,		NULL,	'h' },
+	{ "up",		no_argument,		NULL,	'u' },
+	{ "down",	no_argument,		NULL,	'd' },
+	{ "format",	required_argument,	NULL,	'f' },
+	{ "now",	required_argument,	NULL,	'n' },
+	{ NULL, 0, NULL, 0 },
+    };
+
+    for (;;) {
+	int c;
+
+	c = getopt_long (argc, argv, "hudf:n:", options, NULL);
+	if (c == -1)
+	    break;
+
+	switch (c) {
+	case 'f':
+	    /* output format */
+	    format = optarg;
+	    break;
+	case 'u':
+	    round = PARSE_TIME_ROUND_UP;
+	    break;
+	case 'd':
+	    round = PARSE_TIME_ROUND_DOWN;
+	    break;
+	case 'n':
+	    /* specify now in seconds since epoch */
+	    now = (time_t) strtol (optarg, NULL, 10);
+	    if (now >= (time_t) 0)
+		nowp = &now;
+	    break;
+	case 'h':
+	case '?':
+	default:
+	    usage (argv[0]);
+	    return 1;
+	}
+    }
+
+    argstr = concat_args (optind, argc, argv);
+    if (!argstr)
+	return 1;
+
+    r = parse_time_string (argstr, &result, nowp, round);
+
+    free (argstr);
+
+    if (r)
+	return 1;
+
+    if (!localtime_r (&result, &tm))
+	return 1;
+
+    strftime (buf, sizeof (buf), format, &tm);
+    printf ("%s\n", buf);
+
+    return 0;
+}
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 4/7] test: add smoke tests for the date/time parser module
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
                   ` (2 preceding siblings ...)
  2012-08-04  7:41 ` [PATCH v2 3/7] test: add new test tool parse-time for date/time parser Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 5/7] lib: add date range query support Jani Nikula
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

Test the date/time parser module directly. Just a small sanity test
initially.
---
 test/notmuch-test      |    1 +
 test/parse-time-string |   26 ++++++++++++++++++++++++++
 2 files changed, 27 insertions(+)
 create mode 100755 test/parse-time-string

diff --git a/test/notmuch-test b/test/notmuch-test
index ea39dfc..80e687e 100755
--- a/test/notmuch-test
+++ b/test/notmuch-test
@@ -59,6 +59,7 @@ TESTS="
   emacs-address-cleaning
   emacs-hello
   emacs-show
+  parse-time-string
 "
 TESTS=${NOTMUCH_TESTS:=$TESTS}
 
diff --git a/test/parse-time-string b/test/parse-time-string
new file mode 100755
index 0000000..34b80d7
--- /dev/null
+++ b/test/parse-time-string
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+test_description="date/time parser module"
+. ./test-lib.sh
+
+# This is currently just a quick sanity/smoke test.
+
+_date ()
+{
+    date -d "$*" +%s
+}
+
+_parse_time ()
+{
+    ${TEST_DIRECTORY}/parse-time --format=%s "$*"
+}
+
+test_begin_subtest "date(1) default format without TZ code"
+test_expect_equal "$(_parse_time Fri Aug 3 23:06:06 2012)" "$(_date Fri Aug 3 23:06:06 2012)"
+
+test_begin_subtest "date(1) --rfc-2822 format"
+test_expect_equal "$(_parse_time Fri, 03 Aug 2012 23:07:46 +0100)" "$(_date Fri, 03 Aug 2012 23:07:46 +0100)"
+
+test_begin_subtest "date(1) --rfc=3339=seconds format"
+test_expect_equal "$(_parse_time 2012-08-03 23:09:37+03:00)" "$(_date 2012-08-03 23:09:37+03:00)"
+
+test_done
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 5/7] lib: add date range query support
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
                   ` (3 preceding siblings ...)
  2012-08-04  7:41 ` [PATCH v2 4/7] test: add smoke tests for the date/time parser module Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 6/7] test: add tests for date:since..until range queries Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 7/7] man: document the " Jani Nikula
  6 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

Add a custom value range processor to enable date and time searches of
the form date:since..until, where "since" and "until" are expressions
understood by the previously added date/time parser, to restrict the
results to messages within a particular time range (based on the Date:
header).

If "since" or "until" describes date/time at an accuracy of days or
less, the values are rounded according to the accuracy, towards past
for "since" and towards future for "until". For example,
date:november..yesterday would match from the beginning of November
until the end of yesterday. Expressions such as date:today..today
means since the beginning of today until the end of today.

Open-ended ranges are supported (since Xapian 1.2.1), i.e. you can
specify date:..until or date:since.. to not limit the start or end
date, respectively.

CAVEATS:

Xapian does not support spaces in range expressions. You can replace
the spaces with '_', or (in most cases) '-', or (in some cases) leave
the spaces out altogether.

Entering date:expr without ".." (for example date:yesterday) will not
work as you might expect. You can achieve the expected result by
duplicating the expr both sides of ".." (for example
date:yesterday..yesterday).

Open-ended ranges won't work with pre-1.2.1 Xapian, but they don't
produce an error either.

Signed-off-by: Jani Nikula <jani@nikula.org>
---
 lib/Makefile.local     |    1 +
 lib/database-private.h |    1 +
 lib/database.cc        |    5 +++++
 lib/parse-time-vrp.cc  |   40 ++++++++++++++++++++++++++++++++++++++++
 lib/parse-time-vrp.h   |   19 +++++++++++++++++++
 5 files changed, 66 insertions(+)
 create mode 100644 lib/parse-time-vrp.cc
 create mode 100644 lib/parse-time-vrp.h

diff --git a/lib/Makefile.local b/lib/Makefile.local
index e29c3a2..e872ea1 100644
--- a/lib/Makefile.local
+++ b/lib/Makefile.local
@@ -59,6 +59,7 @@ libnotmuch_c_srcs =		\
 
 libnotmuch_cxx_srcs =		\
 	$(dir)/database.cc	\
+	$(dir)/parse-time-vrp.cc	\
 	$(dir)/directory.cc	\
 	$(dir)/index.cc		\
 	$(dir)/message.cc	\
diff --git a/lib/database-private.h b/lib/database-private.h
index 88532d5..d3e65fd 100644
--- a/lib/database-private.h
+++ b/lib/database-private.h
@@ -52,6 +52,7 @@ struct _notmuch_database {
     Xapian::QueryParser *query_parser;
     Xapian::TermGenerator *term_gen;
     Xapian::ValueRangeProcessor *value_range_processor;
+    Xapian::ValueRangeProcessor *date_range_processor;
 };
 
 /* Return the list of terms from the given iterator matching a prefix.
diff --git a/lib/database.cc b/lib/database.cc
index 761dc1a..4df3217 100644
--- a/lib/database.cc
+++ b/lib/database.cc
@@ -19,6 +19,7 @@
  */
 
 #include "database-private.h"
+#include "parse-time-vrp.h"
 
 #include <iostream>
 
@@ -710,12 +711,14 @@ notmuch_database_open (const char *path,
 	notmuch->term_gen = new Xapian::TermGenerator;
 	notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
 	notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+	notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
 
 	notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
 	notmuch->query_parser->set_database (*notmuch->xapian_db);
 	notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
 	notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
 	notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
+	notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor);
 
 	for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
 	    prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
@@ -778,6 +781,8 @@ notmuch_database_close (notmuch_database_t *notmuch)
     notmuch->xapian_db = NULL;
     delete notmuch->value_range_processor;
     notmuch->value_range_processor = NULL;
+    delete notmuch->date_range_processor;
+    notmuch->date_range_processor = NULL;
 }
 
 void
diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc
new file mode 100644
index 0000000..148c117
--- /dev/null
+++ b/lib/parse-time-vrp.cc
@@ -0,0 +1,40 @@
+
+#include "database-private.h"
+#include "parse-time-vrp.h"
+#include "parse-time-string.h"
+
+#define PREFIX "date:"
+
+/* See *ValueRangeProcessor in xapian-core/api/valuerangeproc.cc */
+Xapian::valueno
+ParseTimeValueRangeProcessor::operator() (std::string &begin, std::string &end)
+{
+    time_t t, now;
+
+    /* Require date: prefix in start of the range... */
+    if (STRNCMP_LITERAL (begin.c_str (), PREFIX))
+	return Xapian::BAD_VALUENO;
+
+    /* ...and remove it. */
+    begin.erase (0, sizeof (PREFIX) - 1);
+
+    /* Use the same 'now' for begin and end. */
+    if (time (&now) == (time_t) -1)
+	return Xapian::BAD_VALUENO;
+
+    if (!begin.empty ()) {
+	if (parse_time_string (begin.c_str (), &t, &now, PARSE_TIME_ROUND_DOWN))
+	    return Xapian::BAD_VALUENO;
+
+	begin.assign (Xapian::sortable_serialise ((double) t));
+    }
+
+    if (!end.empty ()) {
+	if (parse_time_string (end.c_str (), &t, &now, PARSE_TIME_ROUND_UP))
+	    return Xapian::BAD_VALUENO;
+
+	end.assign (Xapian::sortable_serialise ((double) t));
+    }
+
+    return valno;
+}
diff --git a/lib/parse-time-vrp.h b/lib/parse-time-vrp.h
new file mode 100644
index 0000000..526c217
--- /dev/null
+++ b/lib/parse-time-vrp.h
@@ -0,0 +1,19 @@
+
+#ifndef NOTMUCH_PARSE_TIME_VRP_H
+#define NOTMUCH_PARSE_TIME_VRP_H
+
+#include <xapian.h>
+
+/* see *ValueRangeProcessor in xapian-core/include/xapian/queryparser.h */
+class ParseTimeValueRangeProcessor : public Xapian::ValueRangeProcessor {
+protected:
+    Xapian::valueno valno;
+
+public:
+    ParseTimeValueRangeProcessor (Xapian::valueno slot_)
+	: valno(slot_) { }
+
+    Xapian::valueno operator() (std::string &begin, std::string &end);
+};
+
+#endif /* NOTMUCH_PARSE_TIME_VRP_H */
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 6/7] test: add tests for date:since..until range queries
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
                   ` (4 preceding siblings ...)
  2012-08-04  7:41 ` [PATCH v2 5/7] lib: add date range query support Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  2012-08-04  7:41 ` [PATCH v2 7/7] man: document the " Jani Nikula
  6 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

A brief initial test set.
---
 test/notmuch-test |    1 +
 test/search-date  |   21 +++++++++++++++++++++
 2 files changed, 22 insertions(+)
 create mode 100755 test/search-date

diff --git a/test/notmuch-test b/test/notmuch-test
index 80e687e..1654eb6 100755
--- a/test/notmuch-test
+++ b/test/notmuch-test
@@ -60,6 +60,7 @@ TESTS="
   emacs-hello
   emacs-show
   parse-time-string
+  search-date
 "
 TESTS=${NOTMUCH_TESTS:=$TESTS}
 
diff --git a/test/search-date b/test/search-date
new file mode 100755
index 0000000..70bcf34
--- /dev/null
+++ b/test/search-date
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+test_description="date:since..until queries"
+. ./test-lib.sh
+
+add_email_corpus
+
+test_begin_subtest "Absolute date range"
+output=$(notmuch search date:2010-12-16..12/16/2010 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)"
+
+test_begin_subtest "Absolute time range with TZ"
+notmuch search date:18-Nov-2009_02:19:26-0800..2009-11-18_04:49:52-06:00 | notmuch_search_sanitize > OUTPUT
+cat <<EOF >EXPECTED
+thread:XXX   2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX   2009-11-18 [1/3] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_done
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 7/7] man: document the date:since..until range queries
  2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
                   ` (5 preceding siblings ...)
  2012-08-04  7:41 ` [PATCH v2 6/7] test: add tests for date:since..until range queries Jani Nikula
@ 2012-08-04  7:41 ` Jani Nikula
  6 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-04  7:41 UTC (permalink / raw)
  To: notmuch

---
 man/man7/notmuch-search-terms.7 |  147 +++++++++++++++++++++++++++++++++++----
 1 file changed, 135 insertions(+), 12 deletions(-)

diff --git a/man/man7/notmuch-search-terms.7 b/man/man7/notmuch-search-terms.7
index b8ab52d..cec3785 100644
--- a/man/man7/notmuch-search-terms.7
+++ b/man/man7/notmuch-search-terms.7
@@ -54,6 +54,8 @@ terms to match against specific portions of an email, (where
 
 	folder:<directory-path>
 
+	date:<since>..<until>
+
 The
 .B from:
 prefix is used to match the name or address of the sender of an email
@@ -104,6 +106,26 @@ contained within particular directories within the mail store. Only
 the directory components below the top-level mail database path are
 available to be searched.
 
+The
+.B date:
+prefix can be used to restrict the results to only messages within a
+particular time range (based on the Date: header) with a range syntax
+of:
+
+	date:<since>..<until>
+
+See \fBDATE AND TIME SEARCH\fR below for details on the range
+expression, and supported syntax for <since> and <until> date and time
+expressions.
+
+The time range can also be specified using timestamps with a syntax
+of:
+
+	<initial-timestamp>..<final-timestamp>
+
+Each timestamp is a number representing the number of seconds since
+1970\-01\-01 00:00:00 UTC.
+
 In addition to individual terms, multiple terms can be
 combined with Boolean operators (
 .BR and ", " or ", " not
@@ -117,20 +139,121 @@ operators, but will have to be protected from interpretation by the
 shell, (such as by putting quotation marks around any parenthesized
 expression).
 
-Finally, results can be restricted to only messages within a
-particular time range, (based on the Date: header) with a syntax of:
+.SH DATE AND TIME SEARCH
 
-	<initial-timestamp>..<final-timestamp>
+This is a non-exhaustive description of the date and time search with
+some pseudo notation. Most of the constructs can be mixed freely, and
+in any order, but the same absolute date or time can't be expressed
+twice.
 
-Each timestamp is a number representing the number of seconds since
-1970\-01\-01 00:00:00 UTC. This is not the most convenient means of
-expressing date ranges, but until notmuch is fixed to accept a more
-convenient form, one can use the date program to construct
-timestamps. For example, with the bash shell the following syntax would
-specify a date range to return messages from 2009\-10\-01 until the
-current time:
-
-	$(date +%s \-d 2009\-10\-01)..$(date +%s)
+.RS 4
+.TP 4
+.B The range expression
+
+date:<since>..<until>
+
+The above expression restricts the results to only messages from
+<since> to <until>, based on the Date: header.
+
+If <since> or <until> describes time at an accuracy of days or less,
+the date/time is rounded, towards past for <since> and towards future
+for <until>, to be inclusive. For example, date:january..february
+matches from the beginning of January until the end of
+February. Similarly, date:yesterday..yesterday matches from the
+beginning of yesterday until the end of yesterday.
+
+Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's
+possible to specify date:..<until> or date:<since>.. to not limit the
+start or end time, respectively. Unfortunately, pre-1.2.1 Xapian does
+not report an error on open ended ranges, but it does not work as
+expected either.
+
+Xapian does not support spaces in range expressions. You can replace
+the spaces with '_', or (in most cases) '-', or (in some cases) leave
+the spaces out altogether.
+
+Entering date:expr without ".." (for example date:yesterday) won't
+work, as it's not interpreted as a range expression at all. You can
+achieve the expected result by duplicating the expr both sides of ".."
+(for example date:yesterday..yesterday).
+.RE
+
+.RS 4
+.TP 4
+.B Relative date and time
+[N|number] (years|months|weeks|days|hours|hrs|minutes|mins|seconds|secs) [...]
+
+All refer to past, can be repeated and will be accumulated.
+
+Units can be abbreviated to any length, with the otherwise ambiguous
+single m being m for minutes and M for months.
+
+Number multiplier can also be written out one, two, ..., ten, dozen,
+hundred. As special cases last means one ("last week") and this means
+zero ("this month").
+
+When combined with absolute date and time, the relative date and time
+specification will be relative from the specified absolute date and
+time.
+
+Examples: 5M2d, two weeks
+.RE
+
+.RS 4
+.TP 4
+.B Supported time formats
+H[H]:MM[:SS] [(am|a.m.|pm|p.m.)]
+
+H[H] (am|a.m.|pm|p.m.)
+
+HHMMSS
+
+now
+
+noon
+
+midnight
+
+Examples: 17:05, 5pm
+.RE
+
+.RS 4
+.TP 4
+.B Supported date formats
+YYYY-MM[-DD]
+
+DD-MM[-[YY]YY]
+
+MM-YYYY
+
+M[M]/D[D][/[YY]YY]
+
+M[M]/YYYY
+
+D[D].M[M][.[YY]YY]
+
+D[D][(st|nd|rd|th)] Mon[thname] [YYYY]
+
+Mon[thname] D[D][(st|nd|rd|th)] [YYYY]
+
+Wee[kday]
+
+Month names can be abbreviated at three or more characters.
+
+Weekday names can be abbreviated at three or more characters.
+
+Examples: 2012-07-31, 31-07-2012, 7/31/2012, August 3
+.RE
+
+.RS 4
+.TP 4
+.B Time zones
+(+|-)HH:MM
+
+(+|-)HH[MM]
+
+Some time zone codes, e.g. UTC, EET.
+.RE
 
 .SH SEE ALSO
 
-- 
1.7.9.5

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 2/7] lib: add a date/time parser module
  2012-08-04  7:41 ` [PATCH v2 2/7] lib: add a date/time parser module Jani Nikula
@ 2012-08-05 13:08   ` David Bremner
  2012-08-05 21:43     ` Jani Nikula
  0 siblings, 1 reply; 10+ messages in thread
From: David Bremner @ 2012-08-05 13:08 UTC (permalink / raw)
  To: Jani Nikula, notmuch

Jani Nikula <jani@nikula.org> writes:

> +
> +static enum field
> +abs_to_rel_field (enum field field)
> +{
> +    assert (field <= TM_ABS_YEAR);
> +
> +    /* note: depends on the enum ordering */
> +    return field + (TM_REL_SEC - TM_ABS_SEC);
> +}
> +

I wonder if this would be slightly nicer of you defined a TM_FIRST_REL
or so as a synonym like TM_NONE and TM_SIZE

> +/* get zero value for field */
> +static int
> +field_zero (enum field field)
> +{
> +    if (field == TM_ABS_MDAY || field == TM_ABS_MON)
> +	return 1;
> +    else if (field == TM_ABS_YEAR)
> +	return 1970;
> +    else
> +	return 0;
> +}

what do you think about using the word "epoch" instead of zero here?

> +static bool
> +get_postponed_number (struct state *state, int *v, int *n, char *d)
> +{

I found the 1 letter names not quite obvious here.

At this point reading the code, I have not trouble understanding each
line/function, but I feel like I'm missing the big picture a bit. 
What is a postponed number?

> +    /*
> +     * REVISIT: There could be a "next_field" that would be set from
> +     * "field" for the duration of the handle_postponed_number() call,
> +     * so it has more information to work with.
> +     */

The notmuch convention seems to be to use XXX: for this. I'm not sure
I'd bother changing, especially if we can't decide how to package this.

> +/* Time set helper. No input checking. Use UNSET (-1) to leave unset. */
> +static int
> +set_abs_time (struct state *state, int hour, int min, int sec)
> +{
> +    int r;
> +
> +    if (hour != UNSET) {
> +	if ((r = set_field (state, TM_ABS_HOUR, hour)))
> +	    return r;
> +    }

So for this function and the next, the first match wins? I don't really
see the motivation for this, maybe you can explain a bit.


> +    /* timezone codes: offset in minutes. FIXME: add more codes. */

Did you think about trying to delegate the list of timezones to the
system?

> + * Compare strings s and keyword. Return number of matching chars on
> + * match, 0 for no match. Match must be at least n chars (n == 0 all
> + * of keyword), otherwise it's not a match. Use match_case for case
> + * sensitive matching.
> + */

I guess that's fine, and it is internal, but maybe -1 for whole string
would be slightly nicer (although I can't imagine what good matching 0
length strings is at the moment).

> +	/* Minimum match length. */
> +	p = strchr (keyword, '|');
> +	if (p) {
> +	    minlen = p - keyword;
> +	    memmove (p, p + 1, strlen (p + 1) + 1);
> +	}

Something about that memmove creeps me out, but I trust you that it's
correct. Alternatively I guess you could represent keywords as pairs of
strings, which is probably more of a pain.


> +
> +/* Parse a single number. Typically postpone parsing until later. */

OK, so I finally start to understand what a postponed number is :)
I understand the compiler likes bottom up declarations, but some
top down declarations/comments are needed I think.

> +static int
> +parse_date (struct state *state, char sep,
> +	    unsigned long v1, unsigned long v2, unsigned long v3,
> +	    size_t n1, size_t n2, size_t n3)
> +{
> +    int year = UNSET, mon = UNSET, mday = UNSET;
> +
> +    assert (is_date_sep (sep));
> +
> +    switch (sep) {
> +    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */

If I understand correctly, this chooses between American (?) month, day,
year ordering and "sensible" day, month, year ordering by delimiter. I
never thought about this as a way to tell (I often write D/M/Y), but
that could be just me. I agree it's fine as a convention.

> +/*
> + * Parse delimiter(s). Return < 0 on error, number of parsed chars on
> + * success.
> + */

So 1:-2 will parse as 1-2 ?, i.e. last delimiter wins? Maybe better to
say so explicitly.

> +/* Combine absolute and relative fields, and round. */
> +static int
> +create_output (struct state *state, time_t *t_out, const time_t *tnow,
> +	       int round)
> +{

It seems like most of non-obvious logic like (when is "wednesday") is
encoded here. From a maintenence point of view, it would be nice to be
able to seperate out the heuristic stuff from the mechanical, to the
degree that it is possible.

d

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 2/7] lib: add a date/time parser module
  2012-08-05 13:08   ` David Bremner
@ 2012-08-05 21:43     ` Jani Nikula
  0 siblings, 0 replies; 10+ messages in thread
From: Jani Nikula @ 2012-08-05 21:43 UTC (permalink / raw)
  To: David Bremner, notmuch


Hi David, thanks for the review!

On Sun, 05 Aug 2012, David Bremner <david@tethera.net> wrote:
> Jani Nikula <jani@nikula.org> writes:
>
>> +
>> +static enum field
>> +abs_to_rel_field (enum field field)
>> +{
>> +    assert (field <= TM_ABS_YEAR);
>> +
>> +    /* note: depends on the enum ordering */
>> +    return field + (TM_REL_SEC - TM_ABS_SEC);
>> +}
>> +
>
> I wonder if this would be slightly nicer of you defined a TM_FIRST_REL
> or so as a synonym like TM_NONE and TM_SIZE

Good idea.

>> +/* get zero value for field */
>> +static int
>> +field_zero (enum field field)
>> +{
>> +    if (field == TM_ABS_MDAY || field == TM_ABS_MON)
>> +	return 1;
>> +    else if (field == TM_ABS_YEAR)
>> +	return 1970;
>> +    else
>> +	return 0;
>> +}
>
> what do you think about using the word "epoch" instead of zero here?

As a non-native speaker, I'll just take your word for it if you think it
would be better. :)

>> +static bool
>> +get_postponed_number (struct state *state, int *v, int *n, char *d)
>> +{
>
> I found the 1 letter names not quite obvious here.

True. I think v and n are used fairly consistently throughout the source
file, so I'll have to consider longer names vs. documentation comment
for them.

> At this point reading the code, I have not trouble understanding each
> line/function, but I feel like I'm missing the big picture a bit. 

Yeah, perhaps the code reads better if you follow from the top level
parse_time_string() down. Which is at the very end. A "big picture"
documentation comment would do no harm.

> What is a postponed number?

I see that you grasped that later, but I'll describe anyway. Parsing is
done from left to right, in a greedy fashion (i.e. match the longest
possible known expression). If after that we encounter a number that we
don't know what to do with yet, postpone it until we move on to the next
expression. The parser for that might eat the preceding "postponed"
number (for example "5 August", 5 would be postponed because we don't
know what to do with yet, but then "August" would be the context to make
it day of month). If that is not the case, the number will be parsed by
parse_postponed_number() as a lonely, single number between there. (Yes,
this should be documented better with the "big picture".)

>
>> +    /*
>> +     * REVISIT: There could be a "next_field" that would be set from
>> +     * "field" for the duration of the handle_postponed_number() call,
>> +     * so it has more information to work with.
>> +     */
>
> The notmuch convention seems to be to use XXX: for this. I'm not sure
> I'd bother changing, especially if we can't decide how to package this.

Okay, can be changed if needed.

>
>> +/* Time set helper. No input checking. Use UNSET (-1) to leave unset. */
>> +static int
>> +set_abs_time (struct state *state, int hour, int min, int sec)
>> +{
>> +    int r;
>> +
>> +    if (hour != UNSET) {
>> +	if ((r = set_field (state, TM_ABS_HOUR, hour)))
>> +	    return r;
>> +    }
>
> So for this function and the next, the first match wins? I don't really
> see the motivation for this, maybe you can explain a bit.

The whole parser tries to be as unambiguous as possible. If the input
leads to a situation in which any absolute time field (see enum field)
is attempted to set twice, it means it appears twice in the input. For
example, "2012-08-06 August", where the month is set twice. By design,
that is not allowed, and set_field() fails, even if it's the same
value. The only semi-exception is having redundant weekday there, for
example "Monday, August 6".

>
>
>> +    /* timezone codes: offset in minutes. FIXME: add more codes. */
>
> Did you think about trying to delegate the list of timezones to the
> system?

No. :) I'll look into it.

>
>> + * Compare strings s and keyword. Return number of matching chars on
>> + * match, 0 for no match. Match must be at least n chars (n == 0 all
>> + * of keyword), otherwise it's not a match. Use match_case for case
>> + * sensitive matching.
>> + */
>
> I guess that's fine, and it is internal, but maybe -1 for whole string
> would be slightly nicer (although I can't imagine what good matching 0
> length strings is at the moment).

Can be changed.

>
>> +	/* Minimum match length. */
>> +	p = strchr (keyword, '|');
>> +	if (p) {
>> +	    minlen = p - keyword;
>> +	    memmove (p, p + 1, strlen (p + 1) + 1);
>> +	}
>
> Something about that memmove creeps me out, but I trust you that it's
> correct. Alternatively I guess you could represent keywords as pairs of
> strings, which is probably more of a pain.

I didn't bother to double check it now, but I remember thinking it over
very carefully. :) I agree it could use more clarity to be more
obviously correct.

(Initially the minlen was coded as an int in the table, but the above
allows the localization to decide how long the match must be.)

>
>
>> +
>> +/* Parse a single number. Typically postpone parsing until later. */
>
> OK, so I finally start to understand what a postponed number is :)
> I understand the compiler likes bottom up declarations, but some
> top down declarations/comments are needed I think.

I agree more comments would be in order.

>
>> +static int
>> +parse_date (struct state *state, char sep,
>> +	    unsigned long v1, unsigned long v2, unsigned long v3,
>> +	    size_t n1, size_t n2, size_t n3)
>> +{
>> +    int year = UNSET, mon = UNSET, mday = UNSET;
>> +
>> +    assert (is_date_sep (sep));
>> +
>> +    switch (sep) {
>> +    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */
>
> If I understand correctly, this chooses between American (?) month, day,
> year ordering and "sensible" day, month, year ordering by delimiter. I
> never thought about this as a way to tell (I often write D/M/Y), but
> that could be just me. I agree it's fine as a convention.

You understand correctly. It's obviously not a reliable way to tell
unless you make it the convention, which I chose to do. Some parsers,
notably the one in git, also look at the values to see if it could be
D/M/Y if M/D/Y is not possible, but I don't like the ambiguity that
introduces.

>
>> +/*
>> + * Parse delimiter(s). Return < 0 on error, number of parsed chars on
>> + * success.
>> + */
>
> So 1:-2 will parse as 1-2 ?, i.e. last delimiter wins? Maybe better to
> say so explicitly.

Agreed. It just throws out any extra delimiters. Perhaps this should
also be more strict about the allowed delimiters (now anything is
allowed).

>
>> +/* Combine absolute and relative fields, and round. */
>> +static int
>> +create_output (struct state *state, time_t *t_out, const time_t *tnow,
>> +	       int round)
>> +{
>
> It seems like most of non-obvious logic like (when is "wednesday") is
> encoded here. From a maintenence point of view, it would be nice to be
> able to seperate out the heuristic stuff from the mechanical, to the
> degree that it is possible.

Agreed. Basically this is step 2, turning all information parsed in step
1 (function parse_input()) into a sensible result. Figuring out the
current time is also done here.


BR,
Jani.

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2012-08-05 21:43 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2012-08-04  7:41 [PATCH v2 0/7] notmuch search date:since..until query support Jani Nikula
2012-08-04  7:41 ` [PATCH v2 1/7] build: drop the -Wswitch-enum warning Jani Nikula
2012-08-04  7:41 ` [PATCH v2 2/7] lib: add a date/time parser module Jani Nikula
2012-08-05 13:08   ` David Bremner
2012-08-05 21:43     ` Jani Nikula
2012-08-04  7:41 ` [PATCH v2 3/7] test: add new test tool parse-time for date/time parser Jani Nikula
2012-08-04  7:41 ` [PATCH v2 4/7] test: add smoke tests for the date/time parser module Jani Nikula
2012-08-04  7:41 ` [PATCH v2 5/7] lib: add date range query support Jani Nikula
2012-08-04  7:41 ` [PATCH v2 6/7] test: add tests for date:since..until range queries Jani Nikula
2012-08-04  7:41 ` [PATCH v2 7/7] man: document the " Jani Nikula

Code repositories for project(s) associated with this public inbox

	https://yhetil.org/notmuch.git/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).