[PATCH v6 0/9] notmuch search date:since..until query support
authorJani Nikula <jani@nikula.org>
Tue, 30 Oct 2012 20:32:31 +0000 (22:32 +0200)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:50:12 +0000 (09:50 -0800)
b6/aa60f6190fcdcfc8053df65d6f0bcfe6e78882 [new file with mode: 0644]

diff --git a/b6/aa60f6190fcdcfc8053df65d6f0bcfe6e78882 b/b6/aa60f6190fcdcfc8053df65d6f0bcfe6e78882
new file mode 100644 (file)
index 0000000..1a77daf
--- /dev/null
@@ -0,0 +1,930 @@
+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