--- /dev/null
+Return-Path: <jani@nikula.org>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+ by olra.theworths.org (Postfix) with ESMTP id 003F3431FBD\r
+ for <notmuch@notmuchmail.org>; Tue, 30 Oct 2012 13:32:50 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -0.7\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5\r
+ tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+ by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+ with ESMTP id exECDZ2NzV9R for <notmuch@notmuchmail.org>;\r
+ Tue, 30 Oct 2012 13:32:48 -0700 (PDT)\r
+Received: from mail-la0-f53.google.com (mail-la0-f53.google.com\r
+ [209.85.215.53]) (using TLSv1 with cipher RC4-SHA (128/128 bits))\r
+ (No client certificate requested)\r
+ by olra.theworths.org (Postfix) with ESMTPS id BDEF1431FAF\r
+ for <notmuch@notmuchmail.org>; Tue, 30 Oct 2012 13:32:47 -0700 (PDT)\r
+Received: by mail-la0-f53.google.com with SMTP id l5so545085lah.26\r
+ for <notmuch@notmuchmail.org>; Tue, 30 Oct 2012 13:32:45 -0700 (PDT)\r
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r
+ d=google.com; s=20120113;\r
+ h=from:to:cc:subject:date:message-id:x-mailer:mime-version\r
+ :content-type:content-transfer-encoding:x-gm-message-state;\r
+ bh=RVN5xU2NymrZhOPv0H0v2TisQVM+C+uv0o5ARy/OObo=;\r
+ b=iUqcWm9lz3yGPuFZVIfTYAa2Naw5GprvgWPmpLdb4G+eyUCZiycWq+FUMF9jHyAY7Q\r
+ zA1TiAd6k9D1wsV3U6NEoegQW34ypbC5ibwrgZ3J3Rs26lRvjMh/tsQw1TlVCyyj/ant\r
+ jZ8eFbIknA7AfH9xoZue8aniEHHjmLKZOyBolzQHqkuYW49Hxsbi9GygYV6brkqCqX1D\r
+ b7/DZhEWfSPMqLBJt4LftKzu2M20DugUkPhudbhweHXdgvJXX44G9IlbOkfrbIxCFH4H\r
+ k/bwA0t5TcWsY1juf27cRzMBkaOYzEhHLPJfDsIZ2lGXtA8oLxvdFUE6BDl9OZnC1UR7\r
+ dvVg==\r
+Received: by 10.152.129.197 with SMTP id ny5mr29467956lab.43.1351629165352;\r
+ Tue, 30 Oct 2012 13:32:45 -0700 (PDT)\r
+Received: from localhost (dsl-hkibrasgw4-fe51df00-27.dhcp.inet.fi.\r
+ [80.223.81.27])\r
+ by mx.google.com with ESMTPS id e4sm756902lby.12.2012.10.30.13.32.41\r
+ (version=SSLv3 cipher=OTHER); Tue, 30 Oct 2012 13:32:43 -0700 (PDT)\r
+From: Jani Nikula <jani@nikula.org>\r
+To: notmuch@notmuchmail.org\r
+Subject: [PATCH v6 0/9] notmuch search date:since..until query support\r
+Date: Tue, 30 Oct 2012 22:32:31 +0200\r
+Message-Id: <cover.1351626272.git.jani@nikula.org>\r
+X-Mailer: git-send-email 1.7.10.4\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=UTF-8\r
+Content-Transfer-Encoding: 8bit\r
+X-Gm-Message-State:\r
+ ALoCoQkQRcUI4et7w5DDMQLuSefLUl2yNxad5jM+csYUyuBkH/wF3H7LeDTBlIFm83LsnC5E7RZ5\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+ <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Tue, 30 Oct 2012 20:32:51 -0000\r
+\r
+Hi all, v6 of [1] with plenty of small changes addressing Austin's\r
+review [2], [3], [4], and [5]. See my replies to Austin for what I've\r
+agreed to change, and what I've chosen to ignore and why.\r
+\r
+The single biggest change is the requirement to have some delimiter(s)\r
+between keywords, which allowed simplification of keyword\r
+matching. Consequently match_keyword() and parse_keyword() functions in\r
+patch 2/9 have changed considerably.\r
+\r
+There are a few ways to examine the changes since v5. My public repo at\r
+[6] has branches topic-parse-time-string-v5 (rebased to master) and\r
+topic-parse-time-string-v6, and [7] should provide a fancy colorful diff\r
+between the two. The same but less fancy diff is also at the end of this\r
+cover letter.\r
+\r
+Change by change commits to the parser and test tool can also be found\r
+at [8]. The source files there are copied verbatim to patches 2/9 and\r
+3/9.\r
+\r
+\r
+BR,\r
+Jani.\r
+\r
+\r
+[1] id:cover.1350854171.git.jani@nikula.org\r
+[2] id:20121022081444.GM14861@mit.edu for patch 2/9\r
+[3] id:20121023042326.GP14861@mit.edu for patch 4/9\r
+[4] id:20121023045255.GQ14861@mit.edu for patch 6/9\r
+[5] id:20121024210841.GU14861@mit.edu for patch 8/9\r
+[6] https://gitorious.org/jani/notmuch\r
+[7] https://gitorious.org/jani/notmuch/commit/06c76eb4181bc88eccabc419c690046682125d7a/diffs/ef5e8d111748784433f4b80c9e5378f0c1a57319\r
+[8] https://gitorious.org/parse-time-string/parse-time-string\r
+\r
+Jani Nikula (9):\r
+ build: drop the -Wswitch-enum warning\r
+ parse-time-string: add a date/time parser to notmuch\r
+ test: add new test tool parse-time for date/time parser\r
+ test: add smoke tests for the date/time parser module\r
+ build: build parse-time-string as part of the notmuch lib and static\r
+ cli\r
+ lib: add date range query support\r
+ test: add tests for date:since..until range queries\r
+ man: document the date:since..until range queries\r
+ NEWS: date range search support\r
+\r
+ Makefile | 2 +-\r
+ Makefile.local | 2 +-\r
+ NEWS | 12 +\r
+ configure | 2 +-\r
+ lib/Makefile.local | 3 +-\r
+ lib/database-private.h | 1 +\r
+ lib/database.cc | 5 +\r
+ lib/parse-time-vrp.cc | 61 ++\r
+ lib/parse-time-vrp.h | 40 +\r
+ man/man7/notmuch-search-terms.7 | 150 +++-\r
+ parse-time-string/Makefile | 5 +\r
+ parse-time-string/Makefile.local | 12 +\r
+ parse-time-string/README | 9 +\r
+ parse-time-string/parse-time-string.c | 1503 +++++++++++++++++++++++++++++++++\r
+ parse-time-string/parse-time-string.h | 102 +++\r
+ test/Makefile.local | 7 +-\r
+ test/basic | 2 +-\r
+ test/notmuch-test | 2 +\r
+ test/parse-time-string | 78 ++\r
+ test/parse-time.c | 314 +++++++\r
+ test/search-date | 21 +\r
+ 21 files changed, 2315 insertions(+), 18 deletions(-)\r
+ create mode 100644 lib/parse-time-vrp.cc\r
+ create mode 100644 lib/parse-time-vrp.h\r
+ create mode 100644 parse-time-string/Makefile\r
+ create mode 100644 parse-time-string/Makefile.local\r
+ create mode 100644 parse-time-string/README\r
+ create mode 100644 parse-time-string/parse-time-string.c\r
+ create mode 100644 parse-time-string/parse-time-string.h\r
+ create mode 100755 test/parse-time-string\r
+ create mode 100644 test/parse-time.c\r
+ create mode 100755 test/search-date\r
+\r
+-- \r
+1.7.10.4\r
+\r
+diff between v5 and v6:\r
+\r
+diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc\r
+index 7e4eca4..33f07db 100644\r
+--- a/lib/parse-time-vrp.cc\r
++++ b/lib/parse-time-vrp.cc\r
+@@ -1,3 +1,24 @@\r
++/* parse-time-vrp.cc - date range query glue\r
++ *\r
++ * This file is part of notmuch.\r
++ *\r
++ * Copyright © 2012 Jani Nikula\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program. If not, see http://www.gnu.org/licenses/ .\r
++ *\r
++ * Author: Jani Nikula <jani@nikula.org>\r
++ */\r
+ \r
+ #include "database-private.h"\r
+ #include "parse-time-vrp.h"\r
+diff --git a/lib/parse-time-vrp.h b/lib/parse-time-vrp.h\r
+index 526c217..094c4f8 100644\r
+--- a/lib/parse-time-vrp.h\r
++++ b/lib/parse-time-vrp.h\r
+@@ -1,3 +1,24 @@\r
++/* parse-time-vrp.h - date range query glue\r
++ *\r
++ * This file is part of notmuch.\r
++ *\r
++ * Copyright © 2012 Jani Nikula\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program. If not, see http://www.gnu.org/licenses/ .\r
++ *\r
++ * Author: Jani Nikula <jani@nikula.org>\r
++ */\r
+ \r
+ #ifndef NOTMUCH_PARSE_TIME_VRP_H\r
+ #define NOTMUCH_PARSE_TIME_VRP_H\r
+diff --git a/man/man7/notmuch-search-terms.7 b/man/man7/notmuch-search-terms.7\r
+index fbd3ee7..e39b944 100644\r
+--- a/man/man7/notmuch-search-terms.7\r
++++ b/man/man7/notmuch-search-terms.7\r
+@@ -141,10 +141,13 @@ expression).\r
+ \r
+ .SH DATE AND TIME SEARCH\r
+ \r
+-This is a non-exhaustive description of the date and time search with\r
+-some pseudo notation. Most of the constructs can be mixed freely, and\r
+-in any order, but the same absolute date or time can't be expressed\r
+-twice.\r
++notmuch understands a variety of standard and natural ways of\r
++expressing dates and times, both in absolute terms ("2012-10-24") and\r
++in relative terms ("yesterday"). Any number of relative terms can be\r
++combined ("1 hour 25 minutes") and an absolute date/time can be\r
++combined with relative terms to further adjust it. A non-exhaustive\r
++description of the syntax supported for absolute and relative terms is\r
++given below.\r
+ \r
+ .RS 4\r
+ .TP 4\r
+@@ -155,22 +158,22 @@ date:<since>..<until>\r
+ The above expression restricts the results to only messages from\r
+ <since> to <until>, based on the Date: header.\r
+ \r
+-If <since> or <until> describes time at an accuracy of days or less,\r
+-the date/time is rounded, towards past for <since> and towards future\r
+-for <until>, to be inclusive. For example, date:january..february\r
+-matches from the beginning of January until the end of\r
+-February. Similarly, date:yesterday..yesterday matches from the\r
+-beginning of yesterday until the end of yesterday.\r
++<since> and <until> can describe imprecise times, such as "yesterday".\r
++In this case, <since> is taken as the earliest time it could describe\r
++(the beginning of yesterday) and <until> is taken as the latest time\r
++it could describe (the end of yesterday). Similarly,\r
++date:january..february matches from the beginning of January to the\r
++end of February.\r
++\r
++Currently, we do not support spaces in range expressions. You can\r
++replace the spaces with '_', or (in most cases) '-', or (in some\r
++cases) leave the spaces out altogether. Examples in this man page use\r
++spaces for clarity.\r
+ \r
+ Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's\r
+ possible to specify date:..<until> or date:<since>.. to not limit the\r
+-start or end time, respectively. Unfortunately, pre-1.2.1 Xapian does\r
+-not report an error on open ended ranges, but it does not work as\r
+-expected either.\r
+-\r
+-Xapian does not support spaces in range expressions. You can replace\r
+-the spaces with '_', or (in most cases) '-', or (in some cases) leave\r
+-the spaces out altogether.\r
++start or end time, respectively. Pre-1.2.1 Xapian does not report an\r
++error on open ended ranges, but it does not work as expected either.\r
+ \r
+ Entering date:expr without ".." (for example date:yesterday) won't\r
+ work, as it's not interpreted as a range expression at all. You can\r
+@@ -188,9 +191,9 @@ All refer to past, can be repeated and will be accumulated.\r
+ Units can be abbreviated to any length, with the otherwise ambiguous\r
+ single m being m for minutes and M for months.\r
+ \r
+-Number multiplier can also be written out one, two, ..., ten, dozen,\r
+-hundred. As special cases last means one ("last week") and this means\r
+-zero ("this month").\r
++Number can also be written out one, two, ..., ten, dozen,\r
++hundred. Additionally, the unit may be preceded by "last" or "this"\r
++(e.g., "last week" or "this month").\r
+ \r
+ When combined with absolute date and time, the relative date and time\r
+ specification will be relative from the specified absolute date and\r
+@@ -201,7 +204,7 @@ Examples: 5M2d, two weeks\r
+ \r
+ .RS 4\r
+ .TP 4\r
+-.B Supported time formats\r
++.B Supported absolute time formats\r
+ H[H]:MM[:SS] [(am|a.m.|pm|p.m.)]\r
+ \r
+ H[H] (am|a.m.|pm|p.m.)\r
+@@ -219,7 +222,7 @@ Examples: 17:05, 5pm\r
+ \r
+ .RS 4\r
+ .TP 4\r
+-.B Supported date formats\r
++.B Supported absolute date formats\r
+ YYYY-MM[-DD]\r
+ \r
+ DD-MM[-[YY]YY]\r
+diff --git a/parse-time-string/parse-time-string.c b/parse-time-string/parse-time-string.c\r
+index 942041a..584067d3 100644\r
+--- a/parse-time-string/parse-time-string.c\r
++++ b/parse-time-string/parse-time-string.c\r
+@@ -120,7 +120,7 @@ enum field {\r
+ TM_ABS_MON, /* month */\r
+ TM_ABS_YEAR, /* year */\r
+ \r
+- TM_ABS_WDAY, /* day of the week. special: may be relative */\r
++ TM_WDAY, /* day of the week. special: may be relative */\r
+ TM_ABS_ISDST, /* daylight saving time */\r
+ \r
+ TM_AMPM, /* am vs. pm */\r
+@@ -165,9 +165,9 @@ abs_to_rel_field (enum field field)\r
+ return field + (TM_FIRST_REL - TM_FIRST_ABS);\r
+ }\r
+ \r
+-/* Get epoch value for field. */\r
++/* Get the smallest acceptable value for field. */\r
+ static int\r
+-field_epoch (enum field field)\r
++get_field_epoch_value (enum field field)\r
+ {\r
+ if (field == TM_ABS_MDAY || field == TM_ABS_MON)\r
+ return 1;\r
+@@ -208,10 +208,11 @@ get_postponed_length (struct state *state)\r
+ * in fact postponed, false otherwise. Store the postponed number's\r
+ * value in *v, length in the input string in *n (or -1 if the number\r
+ * was written out and parsed as a keyword), and the preceding\r
+- * delimiter to *d.\r
++ * delimiter to *d. If a number was not postponed, *v, *n and *d are\r
++ * unchanged.\r
+ */\r
+ static bool\r
+-get_postponed_number (struct state *state, int *v, int *n, char *d)\r
++consume_postponed_number (struct state *state, int *v, int *n, char *d)\r
+ {\r
+ if (!state->postponed_length)\r
+ return false;\r
+@@ -279,8 +280,7 @@ is_field_set (struct state *state, enum field field)\r
+ {\r
+ assert (field < ARRAY_SIZE (state->tm));\r
+ \r
+- return field < ARRAY_SIZE (state->set) &&\r
+- state->set[field] != FIELD_UNSET;\r
++ return state->set[field] != FIELD_UNSET;\r
+ }\r
+ \r
+ static void\r
+@@ -301,10 +301,8 @@ set_field (struct state *state, enum field field, int value)\r
+ {\r
+ int r;\r
+ \r
+- assert (field < ARRAY_SIZE (state->tm));\r
+-\r
+ /* Fields can only be set once. */\r
+- if (field < ARRAY_SIZE (state->set) && state->set[field] != FIELD_UNSET)\r
++ if (is_field_set (state, field))\r
+ return -PARSE_TIME_ERR_ALREADYSET;\r
+ \r
+ state->set[field] = FIELD_SET;\r
+@@ -347,14 +345,13 @@ set_fields_to_now (struct state *state, enum field *fields, size_t n)\r
+ /* Modify field by adding value to it. To be used on relative fields,\r
+ * which can be modified multiple times (to accumulate). */\r
+ static int\r
+-mod_field (struct state *state, enum field field, int value)\r
++add_to_field (struct state *state, enum field field, int value)\r
+ {\r
+ int r;\r
+ \r
+- assert (field < ARRAY_SIZE (state->tm)); /* assert relative??? */\r
++ assert (field < ARRAY_SIZE (state->tm));\r
+ \r
+- if (field < ARRAY_SIZE (state->set))\r
+- state->set[field] = FIELD_SET;\r
++ state->set[field] = FIELD_SET;\r
+ \r
+ /* Parse a previously postponed number, if any. */\r
+ r = parse_postponed_number (state, field);\r
+@@ -387,7 +384,7 @@ get_field (struct state *state, enum field field)\r
+ */\r
+ static bool is_valid_12hour (int h)\r
+ {\r
+- return h >= 0 && h <= 12;\r
++ return h >= 1 && h <= 12;\r
+ }\r
+ \r
+ static bool is_valid_time (int h, int m, int s)\r
+@@ -487,21 +484,15 @@ struct keyword {\r
+ * Setter callback functions for keywords.\r
+ */\r
+ static int\r
+-kw_set_default (struct state *state, struct keyword *kw)\r
+-{\r
+- return set_field (state, kw->field, kw->value);\r
+-}\r
+-\r
+-static int\r
+ kw_set_rel (struct state *state, struct keyword *kw)\r
+ {\r
+ int multiplier = 1;\r
+ \r
+ /* Get a previously set multiplier, if any. */\r
+- get_postponed_number (state, &multiplier, NULL, NULL);\r
++ consume_postponed_number (state, &multiplier, NULL, NULL);\r
+ \r
+ /* Accumulate relative field values. */\r
+- return mod_field (state, kw->field, multiplier * kw->value);\r
++ return add_to_field (state, kw->field, multiplier * kw->value);\r
+ }\r
+ \r
+ static int\r
+@@ -521,7 +512,7 @@ kw_set_month (struct state *state, struct keyword *kw)\r
+ if (n == 1 || n == 2) {\r
+ int r, v;\r
+ \r
+- get_postponed_number (state, &v, NULL, NULL);\r
++ consume_postponed_number (state, &v, NULL, NULL);\r
+ \r
+ if (!is_valid_mday (v))\r
+ return -PARSE_TIME_ERR_INVALIDDATE;\r
+@@ -544,7 +535,7 @@ kw_set_ampm (struct state *state, struct keyword *kw)\r
+ if (n == 1 || n == 2) {\r
+ int r, v;\r
+ \r
+- get_postponed_number (state, &v, NULL, NULL);\r
++ consume_postponed_number (state, &v, NULL, NULL);\r
+ \r
+ if (!is_valid_12hour (v))\r
+ return -PARSE_TIME_ERR_INVALIDTIME;\r
+@@ -585,7 +576,7 @@ kw_set_ordinal (struct state *state, struct keyword *kw)\r
+ int n, v;\r
+ \r
+ /* Require a postponed number. */\r
+- if (!get_postponed_number (state, &v, &n, NULL))\r
++ if (!consume_postponed_number (state, &v, &n, NULL))\r
+ return -PARSE_TIME_ERR_DATEFORMAT;\r
+ \r
+ /* Ordinals are mday. */\r
+@@ -605,32 +596,38 @@ kw_set_ordinal (struct state *state, struct keyword *kw)\r
+ return set_field (state, TM_ABS_MDAY, v);\r
+ }\r
+ \r
++static int\r
++kw_ignore (unused (struct state *state), unused (struct keyword *kw))\r
++{\r
++ return 0;\r
++}\r
++\r
+ /*\r
+ * Accepted keywords.\r
+ *\r
+ * A keyword may optionally contain a '|' to indicate the minimum\r
+ * match length. Without one, full match is required. It's advisable\r
+- * to keep the minimum match parts unique across all keywords.\r
++ * to keep the minimum match parts unique across all keywords. If\r
++ * they're not, the first match wins.\r
+ *\r
+- * If keyword begins with upper case letter, then the matching will be\r
+- * case sensitive. Otherwise the matching is case insensitive.\r
++ * If keyword begins with '*', then the matching will be case\r
++ * sensitive. Otherwise the matching is case insensitive.\r
+ *\r
+- * If setter is NULL, set_default will be used.\r
++ * If .set is NULL, the field specified by .field will be set to\r
++ * .value.\r
+ *\r
+- * Note: Order matters. Matching is greedy, longest match is used, but\r
+- * of equal length matches the first one is used, unless there's an\r
+- * equal length case sensitive match which trumps case insensitive\r
+- * matches.\r
++ * Note: Observe how "m" and "mi" match minutes, "M" and "mo" and\r
++ * "mont" match months, but "mon" matches Monday.\r
+ */\r
+ static struct keyword keywords[] = {\r
+ /* Weekdays. */\r
+- { N_("sun|day"), TM_ABS_WDAY, 0, NULL },\r
+- { N_("mon|day"), TM_ABS_WDAY, 1, NULL },\r
+- { N_("tue|sday"), TM_ABS_WDAY, 2, NULL },\r
+- { N_("wed|nesday"), TM_ABS_WDAY, 3, NULL },\r
+- { N_("thu|rsday"), TM_ABS_WDAY, 4, NULL },\r
+- { N_("fri|day"), TM_ABS_WDAY, 5, NULL },\r
+- { N_("sat|urday"), TM_ABS_WDAY, 6, NULL },\r
++ { N_("sun|day"), TM_WDAY, 0, NULL },\r
++ { N_("mon|day"), TM_WDAY, 1, NULL },\r
++ { N_("tue|sday"), TM_WDAY, 2, NULL },\r
++ { N_("wed|nesday"), TM_WDAY, 3, NULL },\r
++ { N_("thu|rsday"), TM_WDAY, 4, NULL },\r
++ { N_("fri|day"), TM_WDAY, 5, NULL },\r
++ { N_("sat|urday"), TM_WDAY, 6, NULL },\r
+ \r
+ /* Months. */\r
+ { N_("jan|uary"), TM_ABS_MON, 1, kw_set_month },\r
+@@ -648,15 +645,15 @@ static struct keyword keywords[] = {\r
+ \r
+ /* Durations. */\r
+ { N_("y|ears"), TM_REL_YEAR, 1, kw_set_rel },\r
++ { N_("mo|nths"), TM_REL_MON, 1, kw_set_rel },\r
++ { N_("*M"), TM_REL_MON, 1, kw_set_rel },\r
+ { N_("w|eeks"), TM_REL_WEEK, 1, kw_set_rel },\r
+ { N_("d|ays"), TM_REL_DAY, 1, kw_set_rel },\r
+ { N_("h|ours"), TM_REL_HOUR, 1, kw_set_rel },\r
+ { N_("hr|s"), TM_REL_HOUR, 1, kw_set_rel },\r
+- { N_("m|inutes"), TM_REL_MIN, 1, kw_set_rel },\r
+- /* M=months, m=minutes */\r
+- { N_("M"), TM_REL_MON, 1, kw_set_rel },\r
++ { N_("mi|nutes"), TM_REL_MIN, 1, kw_set_rel },\r
+ { N_("mins"), TM_REL_MIN, 1, kw_set_rel },\r
+- { N_("mo|nths"), TM_REL_MON, 1, kw_set_rel },\r
++ { N_("*m"), TM_REL_MIN, 1, kw_set_rel },\r
+ { N_("s|econds"), TM_REL_SEC, 1, kw_set_rel },\r
+ { N_("secs"), TM_REL_SEC, 1, kw_set_rel },\r
+ \r
+@@ -692,6 +689,7 @@ static struct keyword keywords[] = {\r
+ { N_("nd"), TM_NONE, 0, kw_set_ordinal },\r
+ { N_("rd"), TM_NONE, 0, kw_set_ordinal },\r
+ { N_("th"), TM_NONE, 0, kw_set_ordinal },\r
++ { N_("ago"), TM_NONE, 0, kw_ignore },\r
+ \r
+ /* Timezone codes: offset in minutes. XXX: Add more codes. */\r
+ { N_("pst"), TM_TZ, -8*60, NULL },\r
+@@ -715,34 +713,61 @@ static struct keyword keywords[] = {\r
+ };\r
+ \r
+ /*\r
+- * Compare strings s and keyword. Return number of matching chars on\r
+- * match, 0 for no match. Match must be at least n chars, or all of\r
+- * keyword if n < 0, otherwise it's not a match. Use match_case for\r
+- * case sensitive matching.\r
++ * Compare strings str and keyword. Return the number of matching\r
++ * chars on match, 0 for no match.\r
++ *\r
++ * All of the alphabetic characters (isalpha) in str up to the first\r
++ * non-alpha character (or end of string) must match the\r
++ * keyword. Consequently, the value returned on match is the number of\r
++ * consecutive alphabetic characters in str.\r
++ *\r
++ * Abbreviated match is accepted if the keyword contains a '|'\r
++ * character, and str matches keyword up to that character. Any alpha\r
++ * characters after that in str must still match the keyword following\r
++ * the '|' character. If no '|' is present, all of keyword must match.\r
++ *\r
++ * Excessive, consecutive, and misplaced (at the beginning or end) '|'\r
++ * characters in keyword are handled gracefully. Only the first one\r
++ * matters.\r
++ *\r
++ * If match_case is true, the matching is case sensitive.\r
+ */\r
+ static size_t\r
+-match_keyword (const char *s, const char *keyword, ssize_t n, bool match_case)\r
++match_keyword (const char *str, const char *keyword, bool match_case)\r
+ {\r
+- ssize_t i;\r
++ const char *s = str;\r
++ bool prefix_matched = false;\r
+ \r
+- if (!n)\r
+- return 0;\r
++ for (;;) {\r
++ while (*keyword == '|') {\r
++ prefix_matched = true;\r
++ keyword++;\r
++ }\r
++\r
++ if (!*s || !isalpha ((unsigned char) *s) || !*keyword)\r
++ break;\r
+ \r
+- for (i = 0; *s && *keyword; i++, s++, keyword++) {\r
+ if (match_case) {\r
+ if (*s != *keyword)\r
+- break;\r
++ return 0;\r
+ } else {\r
+ if (tolower ((unsigned char) *s) !=\r
+ tolower ((unsigned char) *keyword))\r
+- break;\r
++ return 0;\r
+ }\r
++ s++;\r
++ keyword++;\r
+ }\r
+ \r
+- if (n > 0)\r
+- return i < n ? 0 : i;\r
+- else\r
+- return *keyword ? 0 : i;\r
++ /* did not match all of the keyword in input string */\r
++ if (*s && isalpha ((unsigned char) *s))\r
++ return 0;\r
++\r
++ /* did not match enough of keyword */\r
++ if (*keyword && !prefix_matched)\r
++ return 0;\r
++\r
++ return s - str;\r
+ }\r
+ \r
+ /*\r
+@@ -753,36 +778,24 @@ static ssize_t\r
+ parse_keyword (struct state *state, const char *s)\r
+ {\r
+ unsigned int i;\r
+- size_t n, max_n = 0;\r
++ size_t n = 0;\r
+ struct keyword *kw = NULL;\r
+ int r;\r
+ \r
+- /* Match longest keyword */\r
+ for (i = 0; i < ARRAY_SIZE (keywords); i++) {\r
+- /* Match case if keyword begins with upper case letter. */\r
+- bool mcase = isupper ((unsigned char) keywords[i].name[0]);\r
+- ssize_t minlen = -1;\r
+- char keyword[128];\r
+- char *p;\r
+-\r
+- strncpy (keyword, _(keywords[i].name), sizeof (keyword));\r
+-\r
+- /* Truncate too long keywords. XXX: Make this dynamic? */\r
+- keyword[sizeof (keyword) - 1] = '\0';\r
++ const char *keyword = _(keywords[i].name);\r
++ bool mcase = false;\r
+ \r
+- /* Minimum match length. */\r
+- p = strchr (keyword, '|');\r
+- if (p) {\r
+- minlen = p - keyword;\r
+-\r
+- /* Remove the minimum match length separator. */\r
+- memmove (p, p + 1, strlen (p + 1) + 1);\r
++ /* Match case if keyword begins with '*'. */\r
++ if (*keyword == '*') {\r
++ mcase = true;\r
++ keyword++;\r
+ }\r
+ \r
+- n = match_keyword (s, keyword, minlen, mcase);\r
+- if (n > max_n || (n == max_n && mcase)) {\r
+- max_n = n;\r
++ n = match_keyword (s, keyword, mcase);\r
++ if (n) {\r
+ kw = &keywords[i];\r
++ break;\r
+ }\r
+ }\r
+ \r
+@@ -792,12 +805,12 @@ parse_keyword (struct state *state, const char *s)\r
+ if (kw->set)\r
+ r = kw->set (state, kw);\r
+ else\r
+- r = kw_set_default (state, kw);\r
++ r = set_field (state, kw->field, kw->value);\r
+ \r
+ if (r < 0)\r
+ return r;\r
+ \r
+- return max_n;\r
++ return n;\r
+ }\r
+ \r
+ /*\r
+@@ -832,7 +845,7 @@ parse_postponed_number (struct state *state, unused (enum field next_field))\r
+ char d;\r
+ \r
+ /* Bail out if there's no postponed number. */\r
+- if (!get_postponed_number (state, &v, &n, &d))\r
++ if (!consume_postponed_number (state, &v, &n, &d))\r
+ return 0;\r
+ \r
+ if (n == 1 || n == 2) {\r
+@@ -884,8 +897,6 @@ parse_postponed_number (struct state *state, unused (enum field next_field))\r
+ return -PARSE_TIME_ERR_INVALIDDATE;\r
+ \r
+ return set_abs_date (state, year, mon, mday);\r
+- } else {\r
+- return -PARSE_TIME_ERR_FORMAT;\r
+ }\r
+ \r
+ return -PARSE_TIME_ERR_FORMAT;\r
+@@ -1100,10 +1111,7 @@ parse_number (struct state *state, const char *s)\r
+ \r
+ v1 = strtoul_len (p, &p, &n1);\r
+ \r
+- if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {\r
+- sep = *p;\r
+- v2 = strtoul_len (p + 1, &p, &n2);\r
+- } else {\r
++ if (!is_sep (*p) || !isdigit ((unsigned char) *(p + 1))) {\r
+ /* A single number. */\r
+ r = parse_single_number (state, v1, n1);\r
+ if (r)\r
+@@ -1112,6 +1120,9 @@ parse_number (struct state *state, const char *s)\r
+ return p - s;\r
+ }\r
+ \r
++ sep = *p;\r
++ v2 = strtoul_len (p + 1, &p, &n2);\r
++\r
+ /* A group of two or three numbers? */\r
+ if (*p == sep && isdigit ((unsigned char) *(p + 1)))\r
+ v3 = strtoul_len (p + 1, &p, &n3);\r
+@@ -1199,12 +1210,12 @@ parse_input (struct state *state, const char *s)\r
+ * non-NULL, otherwise current time.\r
+ */\r
+ static int\r
+-initialize_now (struct state *state, struct tm *tm, const time_t *now)\r
++initialize_now (struct state *state, const time_t *ref, struct tm *tm)\r
+ {\r
+ time_t t;\r
+ \r
+- if (now) {\r
+- t = *now;\r
++ if (ref) {\r
++ t = *ref;\r
+ } else {\r
+ if (time (&t) == (time_t) -1)\r
+ return -PARSE_TIME_ERR_LIB;\r
+@@ -1229,9 +1240,14 @@ initialize_now (struct state *state, struct tm *tm, const time_t *now)\r
+ }\r
+ \r
+ /*\r
+- * Normalize tm according to mktime(3). Both mktime(3) and\r
+- * localtime_r(3) use local time, but they cancel each other out here,\r
+- * making this function agnostic to time zone.\r
++ * Normalize tm according to mktime(3); if structure members are\r
++ * outside their valid interval, they will be normalized (so that, for\r
++ * example, 40 October is changed into 9 November), and tm_wday and\r
++ * tm_yday are set to values determined from the contents of the other\r
++ * fields.\r
++ *\r
++ * Both mktime(3) and localtime_r(3) use local time, but they cancel\r
++ * each other out here, making this function agnostic to time zone.\r
+ */\r
+ static int\r
+ normalize_tm (struct tm *tm)\r
+@@ -1258,7 +1274,7 @@ tm_get_field (const struct tm *tm, enum field field)\r
+ case TM_ABS_MDAY: return tm->tm_mday;\r
+ case TM_ABS_MON: return tm->tm_mon + 1; /* 0- to 1-based */\r
+ case TM_ABS_YEAR: return 1900 + tm->tm_year;\r
+- case TM_ABS_WDAY: return tm->tm_wday;\r
++ case TM_WDAY: return tm->tm_wday;\r
+ case TM_ABS_ISDST: return tm->tm_isdst;\r
+ default:\r
+ assert (false);\r
+@@ -1294,7 +1310,7 @@ fixup_ampm (struct state *state)\r
+ hdiff = -12;\r
+ }\r
+ \r
+- mod_field (state, TM_REL_HOUR, -hdiff);\r
++ add_to_field (state, TM_REL_HOUR, -hdiff);\r
+ \r
+ return 0;\r
+ }\r
+@@ -1311,7 +1327,7 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
+ int r;\r
+ int week_round = PARSE_TIME_NO_ROUND;\r
+ \r
+- r = initialize_now (state, &now, ref);\r
++ r = initialize_now (state, ref, &now);\r
+ if (r)\r
+ return r;\r
+ \r
+@@ -1330,10 +1346,10 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
+ * months ago wasn't the same day as today. Postpone until we know\r
+ * date?\r
+ */\r
+- if (is_field_set (state, TM_ABS_WDAY) &&\r
++ if (is_field_set (state, TM_WDAY) &&\r
+ !is_field_set (state, TM_ABS_MDAY)) {\r
+- int wday = get_field (state, TM_ABS_WDAY);\r
+- int today = tm_get_field (&now, TM_ABS_WDAY);\r
++ int wday = get_field (state, TM_WDAY);\r
++ int today = tm_get_field (&now, TM_WDAY);\r
+ int rel_days;\r
+ \r
+ if (today > wday)\r
+@@ -1342,9 +1358,9 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
+ rel_days = today + 7 - wday;\r
+ \r
+ /* This also prevents special week rounding from happening. */\r
+- mod_field (state, TM_REL_DAY, rel_days);\r
++ add_to_field (state, TM_REL_DAY, rel_days);\r
+ \r
+- unset_field (state, TM_ABS_WDAY);\r
++ unset_field (state, TM_WDAY);\r
+ }\r
+ \r
+ r = fixup_ampm (state);\r
+@@ -1361,9 +1377,19 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
+ \r
+ if (is_field_set (state, f) || is_field_set (state, r)) {\r
+ if (round >= PARSE_TIME_ROUND_UP && f != TM_ABS_SEC) {\r
+- mod_field (state, r, -1);\r
++ /*\r
++ * This is the most accurate field\r
++ * specified. Round up adjusting it towards\r
++ * future.\r
++ */\r
++ add_to_field (state, r, -1);\r
++\r
++ /*\r
++ * Go back a second if the result is to be used\r
++ * for inclusive comparisons.\r
++ */\r
+ if (round == PARSE_TIME_ROUND_UP_INCLUSIVE)\r
+- mod_field (state, TM_REL_SEC, 1);\r
++ add_to_field (state, TM_REL_SEC, 1);\r
+ }\r
+ round = PARSE_TIME_NO_ROUND; /* No more rounding. */\r
+ } else {\r
+@@ -1373,7 +1399,7 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
+ week_round = round;\r
+ round = PARSE_TIME_NO_ROUND;\r
+ } else {\r
+- set_field (state, f, field_epoch (f));\r
++ set_field (state, f, get_field_epoch_value (f));\r
+ }\r
+ }\r
+ }\r
+diff --git a/test/parse-time-string b/test/parse-time-string\r
+index 862e701..8ae0b4c 100755\r
+--- a/test/parse-time-string\r
++++ b/test/parse-time-string\r
+@@ -27,19 +27,24 @@ test_begin_subtest "Date parser tests"\r
+ REFERENCE=$(_date Tue Jan 11 11:11:00 +0000 2011)\r
+ cat <<EOF > INPUT\r
+ now ==> Tue Jan 11 11:11:00 +0000 2011\r
+-2010-1-1 ==> ERROR: 5\r
++2010-1-1 ==> ERROR: DATEFORMAT\r
+ Jan 2 ==> Sun Jan 02 11:11:00 +0000 2011\r
+ Mon ==> Mon Jan 10 11:11:00 +0000 2011\r
+-last Friday ==> ERROR: 4\r
+-2 hours ago ==> ERROR: 1\r
++last Friday ==> ERROR: FORMAT\r
++2 hours ago ==> Tue Jan 11 09:11:00 +0000 2011\r
+ last month ==> Sat Dec 11 11:11:00 +0000 2010\r
+-month ago ==> ERROR: 1\r
++month ago ==> Sat Dec 11 11:11:00 +0000 2010\r
++two mo ==> Thu Nov 11 11:11:00 +0000 2010\r
++3M ==> Mon Oct 11 11:11:00 +0000 2010\r
++4-mont ==> Sat Sep 11 11:11:00 +0000 2010\r
++5m ==> Tue Jan 11 11:06:00 +0000 2011\r
++dozen mi ==> Tue Jan 11 10:59:00 +0000 2011\r
+ 8am ==> Tue Jan 11 08:00:00 +0000 2011\r
+ 9:15 ==> Tue Jan 11 09:15:00 +0000 2011\r
+ 12:34 ==> Tue Jan 11 12:34:00 +0000 2011\r
+ monday ==> Mon Jan 10 11:11:00 +0000 2011\r
+ yesterday ==> Mon Jan 10 11:11:00 +0000 2011\r
+-tomorrow ==> ERROR: 1\r
++tomorrow ==> ERROR: KEYWORD\r
+ ==> Tue Jan 11 11:11:00 +0000 2011 # empty string is reference time\r
+ \r
+ Aug 3 23:06:06 2012 ==> Fri Aug 03 23:06:06 +0000 2012 # date(1) default format without TZ code\r
+@@ -52,13 +57,15 @@ Fri, 03 Aug 2012 23:07:46 +0100 ==> Fri Aug 03 22:07:46 +0000 2012 # rfc-2822\r
+ \r
+ 19701223 +0100 ==> Wed Dec 23 11:11:00 +0000 1970 # Timezone is ignored without an error\r
+ \r
++today ==^^> Wed Jan 12 00:00:00 +0000 2011\r
+ today ==^> Tue Jan 11 23:59:59 +0000 2011\r
+ today ==_> Tue Jan 11 00:00:00 +0000 2011\r
+ \r
+-thisweek ==^> Sat Jan 15 23:59:59 +0000 2011\r
+-thisweek ==_> Sun Jan 09 00:00:00 +0000 2011\r
++this week ==^^> Sun Jan 16 00:00:00 +0000 2011\r
++this week ==^> Sat Jan 15 23:59:59 +0000 2011\r
++this week ==_> Sun Jan 09 00:00:00 +0000 2011\r
+ \r
+-two months ago==> ERROR: 1 # "ago" is not supported\r
++two months ago ==> Thu Nov 11 11:11:00 +0000 2010\r
+ two months ==> Thu Nov 11 11:11:00 +0000 2010\r
+ \r
+ @1348569850 ==> Tue Sep 25 10:44:10 +0000 2012\r
+diff --git a/test/parse-time.c b/test/parse-time.c\r
+index 5f73b85..901a4dd 100644\r
+--- a/test/parse-time.c\r
++++ b/test/parse-time.c\r
+@@ -29,6 +29,28 @@\r
+ \r
+ #define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))\r
+ \r
++static const char *parse_time_error_strings[] = {\r
++ [PARSE_TIME_OK] = "OK",\r
++ [PARSE_TIME_ERR] = "ERR",\r
++ [PARSE_TIME_ERR_LIB] = "LIB",\r
++ [PARSE_TIME_ERR_ALREADYSET] = "ALREADYSET",\r
++ [PARSE_TIME_ERR_FORMAT] = "FORMAT",\r
++ [PARSE_TIME_ERR_DATEFORMAT] = "DATEFORMAT",\r
++ [PARSE_TIME_ERR_TIMEFORMAT] = "TIMEFORMAT",\r
++ [PARSE_TIME_ERR_INVALIDDATE] = "INVALIDDATE",\r
++ [PARSE_TIME_ERR_INVALIDTIME] = "INVALIDTIME",\r
++ [PARSE_TIME_ERR_KEYWORD] = "KEYWORD",\r
++};\r
++\r
++static const char *\r
++parse_time_strerror (unsigned int errnum)\r
++{\r
++ if (errnum < ARRAY_SIZE (parse_time_error_strings))\r
++ return parse_time_error_strings[errnum];\r
++ else\r
++ return NULL;\r
++}\r
++\r
+ /*\r
+ * concat argv[start]...argv[end - 1], separating them by a single\r
+ * space, to a malloced string\r
+@@ -188,7 +210,11 @@ parse_stdin (FILE *infile, time_t *ref, int round, const char *format)\r
+ \r
+ strftime (result, sizeof (result), format, &tm);\r
+ } else {\r
+- snprintf (result, sizeof (result), "ERROR: %d", r);\r
++ const char *errstr = parse_time_strerror (r);\r
++ if (errstr)\r
++ snprintf (result, sizeof (result), "ERROR: %s", errstr);\r
++ else\r
++ snprintf (result, sizeof (result), "ERROR: %d", r);\r
+ }\r
+ \r
+ printf ("%s%s %s%s", input, oper, result, trail);\r
+@@ -268,8 +294,15 @@ main (int argc, char *argv[])\r
+ \r
+ free (argstr);\r
+ \r
+- if (r)\r
+- return 1;\r
++ if (r) {\r
++ const char *errstr = parse_time_strerror (r);\r
++ if (errstr)\r
++ fprintf (stderr, "ERROR: %s\n", errstr);\r
++ else\r
++ fprintf (stderr, "ERROR: %d\n", r);\r
++\r
++ return r;\r
++ }\r
+ \r
+ if (!localtime_r (&result, &tm))\r
+ return 1;\r