Re: [PATCH 0/4] Allow specifying alternate names for addresses in other_email
[notmuch-archives.git] / b6 / aa60f6190fcdcfc8053df65d6f0bcfe6e78882
1 Return-Path: <jani@nikula.org>\r
2 X-Original-To: notmuch@notmuchmail.org\r
3 Delivered-To: notmuch@notmuchmail.org\r
4 Received: from localhost (localhost [127.0.0.1])\r
5         by olra.theworths.org (Postfix) with ESMTP id 003F3431FBD\r
6         for <notmuch@notmuchmail.org>; Tue, 30 Oct 2012 13:32:50 -0700 (PDT)\r
7 X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
8 X-Spam-Flag: NO\r
9 X-Spam-Score: -0.7\r
10 X-Spam-Level: \r
11 X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5\r
12         tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled\r
13 Received: from olra.theworths.org ([127.0.0.1])\r
14         by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
15         with ESMTP id exECDZ2NzV9R for <notmuch@notmuchmail.org>;\r
16         Tue, 30 Oct 2012 13:32:48 -0700 (PDT)\r
17 Received: from mail-la0-f53.google.com (mail-la0-f53.google.com\r
18         [209.85.215.53]) (using TLSv1 with cipher RC4-SHA (128/128 bits))\r
19         (No client certificate requested)\r
20         by olra.theworths.org (Postfix) with ESMTPS id BDEF1431FAF\r
21         for <notmuch@notmuchmail.org>; Tue, 30 Oct 2012 13:32:47 -0700 (PDT)\r
22 Received: by mail-la0-f53.google.com with SMTP id l5so545085lah.26\r
23         for <notmuch@notmuchmail.org>; Tue, 30 Oct 2012 13:32:45 -0700 (PDT)\r
24 X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r
25         d=google.com; s=20120113;\r
26         h=from:to:cc:subject:date:message-id:x-mailer:mime-version\r
27         :content-type:content-transfer-encoding:x-gm-message-state;\r
28         bh=RVN5xU2NymrZhOPv0H0v2TisQVM+C+uv0o5ARy/OObo=;\r
29         b=iUqcWm9lz3yGPuFZVIfTYAa2Naw5GprvgWPmpLdb4G+eyUCZiycWq+FUMF9jHyAY7Q\r
30         zA1TiAd6k9D1wsV3U6NEoegQW34ypbC5ibwrgZ3J3Rs26lRvjMh/tsQw1TlVCyyj/ant\r
31         jZ8eFbIknA7AfH9xoZue8aniEHHjmLKZOyBolzQHqkuYW49Hxsbi9GygYV6brkqCqX1D\r
32         b7/DZhEWfSPMqLBJt4LftKzu2M20DugUkPhudbhweHXdgvJXX44G9IlbOkfrbIxCFH4H\r
33         k/bwA0t5TcWsY1juf27cRzMBkaOYzEhHLPJfDsIZ2lGXtA8oLxvdFUE6BDl9OZnC1UR7\r
34         dvVg==\r
35 Received: by 10.152.129.197 with SMTP id ny5mr29467956lab.43.1351629165352;\r
36         Tue, 30 Oct 2012 13:32:45 -0700 (PDT)\r
37 Received: from localhost (dsl-hkibrasgw4-fe51df00-27.dhcp.inet.fi.\r
38         [80.223.81.27])\r
39         by mx.google.com with ESMTPS id e4sm756902lby.12.2012.10.30.13.32.41\r
40         (version=SSLv3 cipher=OTHER); Tue, 30 Oct 2012 13:32:43 -0700 (PDT)\r
41 From: Jani Nikula <jani@nikula.org>\r
42 To: notmuch@notmuchmail.org\r
43 Subject: [PATCH v6 0/9] notmuch search date:since..until query support\r
44 Date: Tue, 30 Oct 2012 22:32:31 +0200\r
45 Message-Id: <cover.1351626272.git.jani@nikula.org>\r
46 X-Mailer: git-send-email 1.7.10.4\r
47 MIME-Version: 1.0\r
48 Content-Type: text/plain; charset=UTF-8\r
49 Content-Transfer-Encoding: 8bit\r
50 X-Gm-Message-State:\r
51  ALoCoQkQRcUI4et7w5DDMQLuSefLUl2yNxad5jM+csYUyuBkH/wF3H7LeDTBlIFm83LsnC5E7RZ5\r
52 X-BeenThere: notmuch@notmuchmail.org\r
53 X-Mailman-Version: 2.1.13\r
54 Precedence: list\r
55 List-Id: "Use and development of the notmuch mail system."\r
56         <notmuch.notmuchmail.org>\r
57 List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
58         <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
59 List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
60 List-Post: <mailto:notmuch@notmuchmail.org>\r
61 List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
62 List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
63         <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
64 X-List-Received-Date: Tue, 30 Oct 2012 20:32:51 -0000\r
65 \r
66 Hi all, v6 of [1] with plenty of small changes addressing Austin's\r
67 review [2], [3], [4], and [5]. See my replies to Austin for what I've\r
68 agreed to change, and what I've chosen to ignore and why.\r
69 \r
70 The single biggest change is the requirement to have some delimiter(s)\r
71 between keywords, which allowed simplification of keyword\r
72 matching. Consequently match_keyword() and parse_keyword() functions in\r
73 patch 2/9 have changed considerably.\r
74 \r
75 There are a few ways to examine the changes since v5. My public repo at\r
76 [6] has branches topic-parse-time-string-v5 (rebased to master) and\r
77 topic-parse-time-string-v6, and [7] should provide a fancy colorful diff\r
78 between the two. The same but less fancy diff is also at the end of this\r
79 cover letter.\r
80 \r
81 Change by change commits to the parser and test tool can also be found\r
82 at [8]. The source files there are copied verbatim to patches 2/9 and\r
83 3/9.\r
84 \r
85 \r
86 BR,\r
87 Jani.\r
88 \r
89 \r
90 [1] id:cover.1350854171.git.jani@nikula.org\r
91 [2] id:20121022081444.GM14861@mit.edu for patch 2/9\r
92 [3] id:20121023042326.GP14861@mit.edu for patch 4/9\r
93 [4] id:20121023045255.GQ14861@mit.edu for patch 6/9\r
94 [5] id:20121024210841.GU14861@mit.edu for patch 8/9\r
95 [6] https://gitorious.org/jani/notmuch\r
96 [7] https://gitorious.org/jani/notmuch/commit/06c76eb4181bc88eccabc419c690046682125d7a/diffs/ef5e8d111748784433f4b80c9e5378f0c1a57319\r
97 [8] https://gitorious.org/parse-time-string/parse-time-string\r
98 \r
99 Jani Nikula (9):\r
100   build: drop the -Wswitch-enum warning\r
101   parse-time-string: add a date/time parser to notmuch\r
102   test: add new test tool parse-time for date/time parser\r
103   test: add smoke tests for the date/time parser module\r
104   build: build parse-time-string as part of the notmuch lib and static\r
105     cli\r
106   lib: add date range query support\r
107   test: add tests for date:since..until range queries\r
108   man: document the date:since..until range queries\r
109   NEWS: date range search support\r
110 \r
111  Makefile                              |    2 +-\r
112  Makefile.local                        |    2 +-\r
113  NEWS                                  |   12 +\r
114  configure                             |    2 +-\r
115  lib/Makefile.local                    |    3 +-\r
116  lib/database-private.h                |    1 +\r
117  lib/database.cc                       |    5 +\r
118  lib/parse-time-vrp.cc                 |   61 ++\r
119  lib/parse-time-vrp.h                  |   40 +\r
120  man/man7/notmuch-search-terms.7       |  150 +++-\r
121  parse-time-string/Makefile            |    5 +\r
122  parse-time-string/Makefile.local      |   12 +\r
123  parse-time-string/README              |    9 +\r
124  parse-time-string/parse-time-string.c | 1503 +++++++++++++++++++++++++++++++++\r
125  parse-time-string/parse-time-string.h |  102 +++\r
126  test/Makefile.local                   |    7 +-\r
127  test/basic                            |    2 +-\r
128  test/notmuch-test                     |    2 +\r
129  test/parse-time-string                |   78 ++\r
130  test/parse-time.c                     |  314 +++++++\r
131  test/search-date                      |   21 +\r
132  21 files changed, 2315 insertions(+), 18 deletions(-)\r
133  create mode 100644 lib/parse-time-vrp.cc\r
134  create mode 100644 lib/parse-time-vrp.h\r
135  create mode 100644 parse-time-string/Makefile\r
136  create mode 100644 parse-time-string/Makefile.local\r
137  create mode 100644 parse-time-string/README\r
138  create mode 100644 parse-time-string/parse-time-string.c\r
139  create mode 100644 parse-time-string/parse-time-string.h\r
140  create mode 100755 test/parse-time-string\r
141  create mode 100644 test/parse-time.c\r
142  create mode 100755 test/search-date\r
143 \r
144 -- \r
145 1.7.10.4\r
146 \r
147 diff between v5 and v6:\r
148 \r
149 diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc\r
150 index 7e4eca4..33f07db 100644\r
151 --- a/lib/parse-time-vrp.cc\r
152 +++ b/lib/parse-time-vrp.cc\r
153 @@ -1,3 +1,24 @@\r
154 +/* parse-time-vrp.cc - date range query glue\r
155 + *\r
156 + * This file is part of notmuch.\r
157 + *\r
158 + * Copyright © 2012 Jani Nikula\r
159 + *\r
160 + * This program is free software: you can redistribute it and/or modify\r
161 + * it under the terms of the GNU General Public License as published by\r
162 + * the Free Software Foundation, either version 3 of the License, or\r
163 + * (at your option) any later version.\r
164 + *\r
165 + * This program is distributed in the hope that it will be useful,\r
166 + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
167 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
168 + * GNU General Public License for more details.\r
169 + *\r
170 + * You should have received a copy of the GNU General Public License\r
171 + * along with this program.  If not, see http://www.gnu.org/licenses/ .\r
172 + *\r
173 + * Author: Jani Nikula <jani@nikula.org>\r
174 + */\r
175  \r
176  #include "database-private.h"\r
177  #include "parse-time-vrp.h"\r
178 diff --git a/lib/parse-time-vrp.h b/lib/parse-time-vrp.h\r
179 index 526c217..094c4f8 100644\r
180 --- a/lib/parse-time-vrp.h\r
181 +++ b/lib/parse-time-vrp.h\r
182 @@ -1,3 +1,24 @@\r
183 +/* parse-time-vrp.h - date range query glue\r
184 + *\r
185 + * This file is part of notmuch.\r
186 + *\r
187 + * Copyright © 2012 Jani Nikula\r
188 + *\r
189 + * This program is free software: you can redistribute it and/or modify\r
190 + * it under the terms of the GNU General Public License as published by\r
191 + * the Free Software Foundation, either version 3 of the License, or\r
192 + * (at your option) any later version.\r
193 + *\r
194 + * This program is distributed in the hope that it will be useful,\r
195 + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
196 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
197 + * GNU General Public License for more details.\r
198 + *\r
199 + * You should have received a copy of the GNU General Public License\r
200 + * along with this program.  If not, see http://www.gnu.org/licenses/ .\r
201 + *\r
202 + * Author: Jani Nikula <jani@nikula.org>\r
203 + */\r
204  \r
205  #ifndef NOTMUCH_PARSE_TIME_VRP_H\r
206  #define NOTMUCH_PARSE_TIME_VRP_H\r
207 diff --git a/man/man7/notmuch-search-terms.7 b/man/man7/notmuch-search-terms.7\r
208 index fbd3ee7..e39b944 100644\r
209 --- a/man/man7/notmuch-search-terms.7\r
210 +++ b/man/man7/notmuch-search-terms.7\r
211 @@ -141,10 +141,13 @@ expression).\r
212  \r
213  .SH DATE AND TIME SEARCH\r
214  \r
215 -This is a non-exhaustive description of the date and time search with\r
216 -some pseudo notation. Most of the constructs can be mixed freely, and\r
217 -in any order, but the same absolute date or time can't be expressed\r
218 -twice.\r
219 +notmuch understands a variety of standard and natural ways of\r
220 +expressing dates and times, both in absolute terms ("2012-10-24") and\r
221 +in relative terms ("yesterday"). Any number of relative terms can be\r
222 +combined ("1 hour 25 minutes") and an absolute date/time can be\r
223 +combined with relative terms to further adjust it. A non-exhaustive\r
224 +description of the syntax supported for absolute and relative terms is\r
225 +given below.\r
226  \r
227  .RS 4\r
228  .TP 4\r
229 @@ -155,22 +158,22 @@ date:<since>..<until>\r
230  The above expression restricts the results to only messages from\r
231  <since> to <until>, based on the Date: header.\r
232  \r
233 -If <since> or <until> describes time at an accuracy of days or less,\r
234 -the date/time is rounded, towards past for <since> and towards future\r
235 -for <until>, to be inclusive. For example, date:january..february\r
236 -matches from the beginning of January until the end of\r
237 -February. Similarly, date:yesterday..yesterday matches from the\r
238 -beginning of yesterday until the end of yesterday.\r
239 +<since> and <until> can describe imprecise times, such as "yesterday".\r
240 +In this case, <since> is taken as the earliest time it could describe\r
241 +(the beginning of yesterday) and <until> is taken as the latest time\r
242 +it could describe (the end of yesterday). Similarly,\r
243 +date:january..february matches from the beginning of January to the\r
244 +end of February.\r
245 +\r
246 +Currently, we do not support spaces in range expressions. You can\r
247 +replace the spaces with '_', or (in most cases) '-', or (in some\r
248 +cases) leave the spaces out altogether. Examples in this man page use\r
249 +spaces for clarity.\r
250  \r
251  Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's\r
252  possible to specify date:..<until> or date:<since>.. to not limit the\r
253 -start or end time, respectively. Unfortunately, pre-1.2.1 Xapian does\r
254 -not report an error on open ended ranges, but it does not work as\r
255 -expected either.\r
256 -\r
257 -Xapian does not support spaces in range expressions. You can replace\r
258 -the spaces with '_', or (in most cases) '-', or (in some cases) leave\r
259 -the spaces out altogether.\r
260 +start or end time, respectively. Pre-1.2.1 Xapian does not report an\r
261 +error on open ended ranges, but it does not work as expected either.\r
262  \r
263  Entering date:expr without ".." (for example date:yesterday) won't\r
264  work, as it's not interpreted as a range expression at all. You can\r
265 @@ -188,9 +191,9 @@ All refer to past, can be repeated and will be accumulated.\r
266  Units can be abbreviated to any length, with the otherwise ambiguous\r
267  single m being m for minutes and M for months.\r
268  \r
269 -Number multiplier can also be written out one, two, ..., ten, dozen,\r
270 -hundred. As special cases last means one ("last week") and this means\r
271 -zero ("this month").\r
272 +Number can also be written out one, two, ..., ten, dozen,\r
273 +hundred. Additionally, the unit may be preceded by "last" or "this"\r
274 +(e.g., "last week" or "this month").\r
275  \r
276  When combined with absolute date and time, the relative date and time\r
277  specification will be relative from the specified absolute date and\r
278 @@ -201,7 +204,7 @@ Examples: 5M2d, two weeks\r
279  \r
280  .RS 4\r
281  .TP 4\r
282 -.B Supported time formats\r
283 +.B Supported absolute time formats\r
284  H[H]:MM[:SS] [(am|a.m.|pm|p.m.)]\r
285  \r
286  H[H] (am|a.m.|pm|p.m.)\r
287 @@ -219,7 +222,7 @@ Examples: 17:05, 5pm\r
288  \r
289  .RS 4\r
290  .TP 4\r
291 -.B Supported date formats\r
292 +.B Supported absolute date formats\r
293  YYYY-MM[-DD]\r
294  \r
295  DD-MM[-[YY]YY]\r
296 diff --git a/parse-time-string/parse-time-string.c b/parse-time-string/parse-time-string.c\r
297 index 942041a..584067d3 100644\r
298 --- a/parse-time-string/parse-time-string.c\r
299 +++ b/parse-time-string/parse-time-string.c\r
300 @@ -120,7 +120,7 @@ enum field {\r
301      TM_ABS_MON,                /* month */\r
302      TM_ABS_YEAR,       /* year */\r
303  \r
304 -    TM_ABS_WDAY,       /* day of the week. special: may be relative */\r
305 +    TM_WDAY,           /* day of the week. special: may be relative */\r
306      TM_ABS_ISDST,      /* daylight saving time */\r
307  \r
308      TM_AMPM,           /* am vs. pm */\r
309 @@ -165,9 +165,9 @@ abs_to_rel_field (enum field field)\r
310      return field + (TM_FIRST_REL - TM_FIRST_ABS);\r
311  }\r
312  \r
313 -/* Get epoch value for field. */\r
314 +/* Get the smallest acceptable value for field. */\r
315  static int\r
316 -field_epoch (enum field field)\r
317 +get_field_epoch_value (enum field field)\r
318  {\r
319      if (field == TM_ABS_MDAY || field == TM_ABS_MON)\r
320         return 1;\r
321 @@ -208,10 +208,11 @@ get_postponed_length (struct state *state)\r
322   * in fact postponed, false otherwise. Store the postponed number's\r
323   * value in *v, length in the input string in *n (or -1 if the number\r
324   * was written out and parsed as a keyword), and the preceding\r
325 - * delimiter to *d.\r
326 + * delimiter to *d. If a number was not postponed, *v, *n and *d are\r
327 + * unchanged.\r
328   */\r
329  static bool\r
330 -get_postponed_number (struct state *state, int *v, int *n, char *d)\r
331 +consume_postponed_number (struct state *state, int *v, int *n, char *d)\r
332  {\r
333      if (!state->postponed_length)\r
334         return false;\r
335 @@ -279,8 +280,7 @@ is_field_set (struct state *state, enum field field)\r
336  {\r
337      assert (field < ARRAY_SIZE (state->tm));\r
338  \r
339 -    return field < ARRAY_SIZE (state->set) &&\r
340 -          state->set[field] != FIELD_UNSET;\r
341 +    return state->set[field] != FIELD_UNSET;\r
342  }\r
343  \r
344  static void\r
345 @@ -301,10 +301,8 @@ set_field (struct state *state, enum field field, int value)\r
346  {\r
347      int r;\r
348  \r
349 -    assert (field < ARRAY_SIZE (state->tm));\r
350 -\r
351      /* Fields can only be set once. */\r
352 -    if (field < ARRAY_SIZE (state->set) && state->set[field] != FIELD_UNSET)\r
353 +    if (is_field_set (state, field))\r
354         return -PARSE_TIME_ERR_ALREADYSET;\r
355  \r
356      state->set[field] = FIELD_SET;\r
357 @@ -347,14 +345,13 @@ set_fields_to_now (struct state *state, enum field *fields, size_t n)\r
358  /* Modify field by adding value to it. To be used on relative fields,\r
359   * which can be modified multiple times (to accumulate). */\r
360  static int\r
361 -mod_field (struct state *state, enum field field, int value)\r
362 +add_to_field (struct state *state, enum field field, int value)\r
363  {\r
364      int r;\r
365  \r
366 -    assert (field < ARRAY_SIZE (state->tm));   /* assert relative??? */\r
367 +    assert (field < ARRAY_SIZE (state->tm));\r
368  \r
369 -    if (field < ARRAY_SIZE (state->set))\r
370 -       state->set[field] = FIELD_SET;\r
371 +    state->set[field] = FIELD_SET;\r
372  \r
373      /* Parse a previously postponed number, if any. */\r
374      r = parse_postponed_number (state, field);\r
375 @@ -387,7 +384,7 @@ get_field (struct state *state, enum field field)\r
376   */\r
377  static bool is_valid_12hour (int h)\r
378  {\r
379 -    return h >= 0 && h <= 12;\r
380 +    return h >= 1 && h <= 12;\r
381  }\r
382  \r
383  static bool is_valid_time (int h, int m, int s)\r
384 @@ -487,21 +484,15 @@ struct keyword {\r
385   * Setter callback functions for keywords.\r
386   */\r
387  static int\r
388 -kw_set_default (struct state *state, struct keyword *kw)\r
389 -{\r
390 -    return set_field (state, kw->field, kw->value);\r
391 -}\r
392 -\r
393 -static int\r
394  kw_set_rel (struct state *state, struct keyword *kw)\r
395  {\r
396      int multiplier = 1;\r
397  \r
398      /* Get a previously set multiplier, if any. */\r
399 -    get_postponed_number (state, &multiplier, NULL, NULL);\r
400 +    consume_postponed_number (state, &multiplier, NULL, NULL);\r
401  \r
402      /* Accumulate relative field values. */\r
403 -    return mod_field (state, kw->field, multiplier * kw->value);\r
404 +    return add_to_field (state, kw->field, multiplier * kw->value);\r
405  }\r
406  \r
407  static int\r
408 @@ -521,7 +512,7 @@ kw_set_month (struct state *state, struct keyword *kw)\r
409      if (n == 1 || n == 2) {\r
410         int r, v;\r
411  \r
412 -       get_postponed_number (state, &v, NULL, NULL);\r
413 +       consume_postponed_number (state, &v, NULL, NULL);\r
414  \r
415         if (!is_valid_mday (v))\r
416             return -PARSE_TIME_ERR_INVALIDDATE;\r
417 @@ -544,7 +535,7 @@ kw_set_ampm (struct state *state, struct keyword *kw)\r
418      if (n == 1 || n == 2) {\r
419         int r, v;\r
420  \r
421 -       get_postponed_number (state, &v, NULL, NULL);\r
422 +       consume_postponed_number (state, &v, NULL, NULL);\r
423  \r
424         if (!is_valid_12hour (v))\r
425             return -PARSE_TIME_ERR_INVALIDTIME;\r
426 @@ -585,7 +576,7 @@ kw_set_ordinal (struct state *state, struct keyword *kw)\r
427      int n, v;\r
428  \r
429      /* Require a postponed number. */\r
430 -    if (!get_postponed_number (state, &v, &n, NULL))\r
431 +    if (!consume_postponed_number (state, &v, &n, NULL))\r
432         return -PARSE_TIME_ERR_DATEFORMAT;\r
433  \r
434      /* Ordinals are mday. */\r
435 @@ -605,32 +596,38 @@ kw_set_ordinal (struct state *state, struct keyword *kw)\r
436      return set_field (state, TM_ABS_MDAY, v);\r
437  }\r
438  \r
439 +static int\r
440 +kw_ignore (unused (struct state *state), unused (struct keyword *kw))\r
441 +{\r
442 +    return 0;\r
443 +}\r
444 +\r
445  /*\r
446   * Accepted keywords.\r
447   *\r
448   * A keyword may optionally contain a '|' to indicate the minimum\r
449   * match length. Without one, full match is required. It's advisable\r
450 - * to keep the minimum match parts unique across all keywords.\r
451 + * to keep the minimum match parts unique across all keywords. If\r
452 + * they're not, the first match wins.\r
453   *\r
454 - * If keyword begins with upper case letter, then the matching will be\r
455 - * case sensitive. Otherwise the matching is case insensitive.\r
456 + * If keyword begins with '*', then the matching will be case\r
457 + * sensitive. Otherwise the matching is case insensitive.\r
458   *\r
459 - * If setter is NULL, set_default will be used.\r
460 + * If .set is NULL, the field specified by .field will be set to\r
461 + * .value.\r
462   *\r
463 - * Note: Order matters. Matching is greedy, longest match is used, but\r
464 - * of equal length matches the first one is used, unless there's an\r
465 - * equal length case sensitive match which trumps case insensitive\r
466 - * matches.\r
467 + * Note: Observe how "m" and "mi" match minutes, "M" and "mo" and\r
468 + * "mont" match months, but "mon" matches Monday.\r
469   */\r
470  static struct keyword keywords[] = {\r
471      /* Weekdays. */\r
472 -    { N_("sun|day"),   TM_ABS_WDAY,    0,      NULL },\r
473 -    { N_("mon|day"),   TM_ABS_WDAY,    1,      NULL },\r
474 -    { N_("tue|sday"),  TM_ABS_WDAY,    2,      NULL },\r
475 -    { N_("wed|nesday"),        TM_ABS_WDAY,    3,      NULL },\r
476 -    { N_("thu|rsday"), TM_ABS_WDAY,    4,      NULL },\r
477 -    { N_("fri|day"),   TM_ABS_WDAY,    5,      NULL },\r
478 -    { N_("sat|urday"), TM_ABS_WDAY,    6,      NULL },\r
479 +    { N_("sun|day"),   TM_WDAY,        0,      NULL },\r
480 +    { N_("mon|day"),   TM_WDAY,        1,      NULL },\r
481 +    { N_("tue|sday"),  TM_WDAY,        2,      NULL },\r
482 +    { N_("wed|nesday"),        TM_WDAY,        3,      NULL },\r
483 +    { N_("thu|rsday"), TM_WDAY,        4,      NULL },\r
484 +    { N_("fri|day"),   TM_WDAY,        5,      NULL },\r
485 +    { N_("sat|urday"), TM_WDAY,        6,      NULL },\r
486  \r
487      /* Months. */\r
488      { N_("jan|uary"),  TM_ABS_MON,     1,      kw_set_month },\r
489 @@ -648,15 +645,15 @@ static struct keyword keywords[] = {\r
490  \r
491      /* Durations. */\r
492      { N_("y|ears"),    TM_REL_YEAR,    1,      kw_set_rel },\r
493 +    { N_("mo|nths"),   TM_REL_MON,     1,      kw_set_rel },\r
494 +    { N_("*M"),                TM_REL_MON,     1,      kw_set_rel },\r
495      { N_("w|eeks"),    TM_REL_WEEK,    1,      kw_set_rel },\r
496      { N_("d|ays"),     TM_REL_DAY,     1,      kw_set_rel },\r
497      { N_("h|ours"),    TM_REL_HOUR,    1,      kw_set_rel },\r
498      { N_("hr|s"),      TM_REL_HOUR,    1,      kw_set_rel },\r
499 -    { N_("m|inutes"),  TM_REL_MIN,     1,      kw_set_rel },\r
500 -    /* M=months, m=minutes */\r
501 -    { N_("M"),         TM_REL_MON,     1,      kw_set_rel },\r
502 +    { N_("mi|nutes"),  TM_REL_MIN,     1,      kw_set_rel },\r
503      { N_("mins"),      TM_REL_MIN,     1,      kw_set_rel },\r
504 -    { N_("mo|nths"),   TM_REL_MON,     1,      kw_set_rel },\r
505 +    { N_("*m"),                TM_REL_MIN,     1,      kw_set_rel },\r
506      { N_("s|econds"),  TM_REL_SEC,     1,      kw_set_rel },\r
507      { N_("secs"),      TM_REL_SEC,     1,      kw_set_rel },\r
508  \r
509 @@ -692,6 +689,7 @@ static struct keyword keywords[] = {\r
510      { N_("nd"),                TM_NONE,        0,      kw_set_ordinal },\r
511      { N_("rd"),                TM_NONE,        0,      kw_set_ordinal },\r
512      { N_("th"),                TM_NONE,        0,      kw_set_ordinal },\r
513 +    { N_("ago"),               TM_NONE,        0,      kw_ignore },\r
514  \r
515      /* Timezone codes: offset in minutes. XXX: Add more codes. */\r
516      { N_("pst"),       TM_TZ,          -8*60,  NULL },\r
517 @@ -715,34 +713,61 @@ static struct keyword keywords[] = {\r
518  };\r
519  \r
520  /*\r
521 - * Compare strings s and keyword. Return number of matching chars on\r
522 - * match, 0 for no match. Match must be at least n chars, or all of\r
523 - * keyword if n < 0, otherwise it's not a match. Use match_case for\r
524 - * case sensitive matching.\r
525 + * Compare strings str and keyword. Return the number of matching\r
526 + * chars on match, 0 for no match.\r
527 + *\r
528 + * All of the alphabetic characters (isalpha) in str up to the first\r
529 + * non-alpha character (or end of string) must match the\r
530 + * keyword. Consequently, the value returned on match is the number of\r
531 + * consecutive alphabetic characters in str.\r
532 + *\r
533 + * Abbreviated match is accepted if the keyword contains a '|'\r
534 + * character, and str matches keyword up to that character. Any alpha\r
535 + * characters after that in str must still match the keyword following\r
536 + * the '|' character. If no '|' is present, all of keyword must match.\r
537 + *\r
538 + * Excessive, consecutive, and misplaced (at the beginning or end) '|'\r
539 + * characters in keyword are handled gracefully. Only the first one\r
540 + * matters.\r
541 + *\r
542 + * If match_case is true, the matching is case sensitive.\r
543   */\r
544  static size_t\r
545 -match_keyword (const char *s, const char *keyword, ssize_t n, bool match_case)\r
546 +match_keyword (const char *str, const char *keyword, bool match_case)\r
547  {\r
548 -    ssize_t i;\r
549 +    const char *s = str;\r
550 +    bool prefix_matched = false;\r
551  \r
552 -    if (!n)\r
553 -       return 0;\r
554 +    for (;;) {\r
555 +       while (*keyword == '|') {\r
556 +           prefix_matched = true;\r
557 +           keyword++;\r
558 +       }\r
559 +\r
560 +       if (!*s || !isalpha ((unsigned char) *s) || !*keyword)\r
561 +           break;\r
562  \r
563 -    for (i = 0; *s && *keyword; i++, s++, keyword++) {\r
564         if (match_case) {\r
565             if (*s != *keyword)\r
566 -               break;\r
567 +               return 0;\r
568         } else {\r
569             if (tolower ((unsigned char) *s) !=\r
570                 tolower ((unsigned char) *keyword))\r
571 -               break;\r
572 +               return 0;\r
573         }\r
574 +       s++;\r
575 +       keyword++;\r
576      }\r
577  \r
578 -    if (n > 0)\r
579 -       return i < n ? 0 : i;\r
580 -    else\r
581 -       return *keyword ? 0 : i;\r
582 +    /* did not match all of the keyword in input string */\r
583 +    if (*s && isalpha ((unsigned char) *s))\r
584 +       return 0;\r
585 +\r
586 +    /* did not match enough of keyword */\r
587 +    if (*keyword && !prefix_matched)\r
588 +       return 0;\r
589 +\r
590 +    return s - str;\r
591  }\r
592  \r
593  /*\r
594 @@ -753,36 +778,24 @@ static ssize_t\r
595  parse_keyword (struct state *state, const char *s)\r
596  {\r
597      unsigned int i;\r
598 -    size_t n, max_n = 0;\r
599 +    size_t n = 0;\r
600      struct keyword *kw = NULL;\r
601      int r;\r
602  \r
603 -    /* Match longest keyword */\r
604      for (i = 0; i < ARRAY_SIZE (keywords); i++) {\r
605 -       /* Match case if keyword begins with upper case letter. */\r
606 -       bool mcase = isupper ((unsigned char) keywords[i].name[0]);\r
607 -       ssize_t minlen = -1;\r
608 -       char keyword[128];\r
609 -       char *p;\r
610 -\r
611 -       strncpy (keyword, _(keywords[i].name), sizeof (keyword));\r
612 -\r
613 -       /* Truncate too long keywords. XXX: Make this dynamic? */\r
614 -       keyword[sizeof (keyword) - 1] = '\0';\r
615 +       const char *keyword = _(keywords[i].name);\r
616 +       bool mcase = false;\r
617  \r
618 -       /* Minimum match length. */\r
619 -       p = strchr (keyword, '|');\r
620 -       if (p) {\r
621 -           minlen = p - keyword;\r
622 -\r
623 -           /* Remove the minimum match length separator. */\r
624 -           memmove (p, p + 1, strlen (p + 1) + 1);\r
625 +       /* Match case if keyword begins with '*'. */\r
626 +       if (*keyword == '*') {\r
627 +           mcase = true;\r
628 +           keyword++;\r
629         }\r
630  \r
631 -       n = match_keyword (s, keyword, minlen, mcase);\r
632 -       if (n > max_n || (n == max_n && mcase)) {\r
633 -           max_n = n;\r
634 +       n = match_keyword (s, keyword, mcase);\r
635 +       if (n) {\r
636             kw = &keywords[i];\r
637 +           break;\r
638         }\r
639      }\r
640  \r
641 @@ -792,12 +805,12 @@ parse_keyword (struct state *state, const char *s)\r
642      if (kw->set)\r
643         r = kw->set (state, kw);\r
644      else\r
645 -       r = kw_set_default (state, kw);\r
646 +       r = set_field (state, kw->field, kw->value);\r
647  \r
648      if (r < 0)\r
649         return r;\r
650  \r
651 -    return max_n;\r
652 +    return n;\r
653  }\r
654  \r
655  /*\r
656 @@ -832,7 +845,7 @@ parse_postponed_number (struct state *state, unused (enum field next_field))\r
657      char d;\r
658  \r
659      /* Bail out if there's no postponed number. */\r
660 -    if (!get_postponed_number (state, &v, &n, &d))\r
661 +    if (!consume_postponed_number (state, &v, &n, &d))\r
662         return 0;\r
663  \r
664      if (n == 1 || n == 2) {\r
665 @@ -884,8 +897,6 @@ parse_postponed_number (struct state *state, unused (enum field next_field))\r
666             return -PARSE_TIME_ERR_INVALIDDATE;\r
667  \r
668         return set_abs_date (state, year, mon, mday);\r
669 -    } else {\r
670 -       return -PARSE_TIME_ERR_FORMAT;\r
671      }\r
672  \r
673      return -PARSE_TIME_ERR_FORMAT;\r
674 @@ -1100,10 +1111,7 @@ parse_number (struct state *state, const char *s)\r
675  \r
676      v1 = strtoul_len (p, &p, &n1);\r
677  \r
678 -    if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {\r
679 -       sep = *p;\r
680 -       v2 = strtoul_len (p + 1, &p, &n2);\r
681 -    } else {\r
682 +    if (!is_sep (*p) || !isdigit ((unsigned char) *(p + 1))) {\r
683         /* A single number. */\r
684         r = parse_single_number (state, v1, n1);\r
685         if (r)\r
686 @@ -1112,6 +1120,9 @@ parse_number (struct state *state, const char *s)\r
687         return p - s;\r
688      }\r
689  \r
690 +    sep = *p;\r
691 +    v2 = strtoul_len (p + 1, &p, &n2);\r
692 +\r
693      /* A group of two or three numbers? */\r
694      if (*p == sep && isdigit ((unsigned char) *(p + 1)))\r
695         v3 = strtoul_len (p + 1, &p, &n3);\r
696 @@ -1199,12 +1210,12 @@ parse_input (struct state *state, const char *s)\r
697   * non-NULL, otherwise current time.\r
698   */\r
699  static int\r
700 -initialize_now (struct state *state, struct tm *tm, const time_t *now)\r
701 +initialize_now (struct state *state, const time_t *ref, struct tm *tm)\r
702  {\r
703      time_t t;\r
704  \r
705 -    if (now) {\r
706 -       t = *now;\r
707 +    if (ref) {\r
708 +       t = *ref;\r
709      } else {\r
710         if (time (&t) == (time_t) -1)\r
711             return -PARSE_TIME_ERR_LIB;\r
712 @@ -1229,9 +1240,14 @@ initialize_now (struct state *state, struct tm *tm, const time_t *now)\r
713  }\r
714  \r
715  /*\r
716 - * Normalize tm according to mktime(3). Both mktime(3) and\r
717 - * localtime_r(3) use local time, but they cancel each other out here,\r
718 - * making this function agnostic to time zone.\r
719 + * Normalize tm according to mktime(3); if structure members are\r
720 + * outside their valid interval, they will be normalized (so that, for\r
721 + * example, 40 October is changed into 9 November), and tm_wday and\r
722 + * tm_yday are set to values determined from the contents of the other\r
723 + * fields.\r
724 + *\r
725 + * Both mktime(3) and localtime_r(3) use local time, but they cancel\r
726 + * each other out here, making this function agnostic to time zone.\r
727   */\r
728  static int\r
729  normalize_tm (struct tm *tm)\r
730 @@ -1258,7 +1274,7 @@ tm_get_field (const struct tm *tm, enum field field)\r
731      case TM_ABS_MDAY:  return tm->tm_mday;\r
732      case TM_ABS_MON:   return tm->tm_mon + 1; /* 0- to 1-based */\r
733      case TM_ABS_YEAR:  return 1900 + tm->tm_year;\r
734 -    case TM_ABS_WDAY:  return tm->tm_wday;\r
735 +    case TM_WDAY:      return tm->tm_wday;\r
736      case TM_ABS_ISDST: return tm->tm_isdst;\r
737      default:\r
738         assert (false);\r
739 @@ -1294,7 +1310,7 @@ fixup_ampm (struct state *state)\r
740             hdiff = -12;\r
741      }\r
742  \r
743 -    mod_field (state, TM_REL_HOUR, -hdiff);\r
744 +    add_to_field (state, TM_REL_HOUR, -hdiff);\r
745  \r
746      return 0;\r
747  }\r
748 @@ -1311,7 +1327,7 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
749      int r;\r
750      int week_round = PARSE_TIME_NO_ROUND;\r
751  \r
752 -    r = initialize_now (state, &now, ref);\r
753 +    r = initialize_now (state, ref, &now);\r
754      if (r)\r
755         return r;\r
756  \r
757 @@ -1330,10 +1346,10 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
758       * months ago wasn't the same day as today. Postpone until we know\r
759       * date?\r
760       */\r
761 -    if (is_field_set (state, TM_ABS_WDAY) &&\r
762 +    if (is_field_set (state, TM_WDAY) &&\r
763         !is_field_set (state, TM_ABS_MDAY)) {\r
764 -       int wday = get_field (state, TM_ABS_WDAY);\r
765 -       int today = tm_get_field (&now, TM_ABS_WDAY);\r
766 +       int wday = get_field (state, TM_WDAY);\r
767 +       int today = tm_get_field (&now, TM_WDAY);\r
768         int rel_days;\r
769  \r
770         if (today > wday)\r
771 @@ -1342,9 +1358,9 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
772             rel_days = today + 7 - wday;\r
773  \r
774         /* This also prevents special week rounding from happening. */\r
775 -       mod_field (state, TM_REL_DAY, rel_days);\r
776 +       add_to_field (state, TM_REL_DAY, rel_days);\r
777  \r
778 -       unset_field (state, TM_ABS_WDAY);\r
779 +       unset_field (state, TM_WDAY);\r
780      }\r
781  \r
782      r = fixup_ampm (state);\r
783 @@ -1361,9 +1377,19 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
784  \r
785             if (is_field_set (state, f) || is_field_set (state, r)) {\r
786                 if (round >= PARSE_TIME_ROUND_UP && f != TM_ABS_SEC) {\r
787 -                   mod_field (state, r, -1);\r
788 +                   /*\r
789 +                    * This is the most accurate field\r
790 +                    * specified. Round up adjusting it towards\r
791 +                    * future.\r
792 +                    */\r
793 +                   add_to_field (state, r, -1);\r
794 +\r
795 +                   /*\r
796 +                    * Go back a second if the result is to be used\r
797 +                    * for inclusive comparisons.\r
798 +                    */\r
799                     if (round == PARSE_TIME_ROUND_UP_INCLUSIVE)\r
800 -                       mod_field (state, TM_REL_SEC, 1);\r
801 +                       add_to_field (state, TM_REL_SEC, 1);\r
802                 }\r
803                 round = PARSE_TIME_NO_ROUND; /* No more rounding. */\r
804             } else {\r
805 @@ -1373,7 +1399,7 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,\r
806                     week_round = round;\r
807                     round = PARSE_TIME_NO_ROUND;\r
808                 } else {\r
809 -                   set_field (state, f, field_epoch (f));\r
810 +                   set_field (state, f, get_field_epoch_value (f));\r
811                 }\r
812             }\r
813         }\r
814 diff --git a/test/parse-time-string b/test/parse-time-string\r
815 index 862e701..8ae0b4c 100755\r
816 --- a/test/parse-time-string\r
817 +++ b/test/parse-time-string\r
818 @@ -27,19 +27,24 @@ test_begin_subtest "Date parser tests"\r
819  REFERENCE=$(_date Tue Jan 11 11:11:00 +0000 2011)\r
820  cat <<EOF > INPUT\r
821  now          ==> Tue Jan 11 11:11:00 +0000 2011\r
822 -2010-1-1     ==> ERROR: 5\r
823 +2010-1-1     ==> ERROR: DATEFORMAT\r
824  Jan 2        ==> Sun Jan 02 11:11:00 +0000 2011\r
825  Mon          ==> Mon Jan 10 11:11:00 +0000 2011\r
826 -last Friday  ==> ERROR: 4\r
827 -2 hours ago  ==> ERROR: 1\r
828 +last Friday  ==> ERROR: FORMAT\r
829 +2 hours ago  ==> Tue Jan 11 09:11:00 +0000 2011\r
830  last month   ==> Sat Dec 11 11:11:00 +0000 2010\r
831 -month ago    ==> ERROR: 1\r
832 +month ago    ==> Sat Dec 11 11:11:00 +0000 2010\r
833 +two mo       ==> Thu Nov 11 11:11:00 +0000 2010\r
834 +3M           ==> Mon Oct 11 11:11:00 +0000 2010\r
835 +4-mont       ==> Sat Sep 11 11:11:00 +0000 2010\r
836 +5m           ==> Tue Jan 11 11:06:00 +0000 2011\r
837 +dozen mi     ==> Tue Jan 11 10:59:00 +0000 2011\r
838  8am          ==> Tue Jan 11 08:00:00 +0000 2011\r
839  9:15         ==> Tue Jan 11 09:15:00 +0000 2011\r
840  12:34        ==> Tue Jan 11 12:34:00 +0000 2011\r
841  monday       ==> Mon Jan 10 11:11:00 +0000 2011\r
842  yesterday    ==> Mon Jan 10 11:11:00 +0000 2011\r
843 -tomorrow     ==> ERROR: 1\r
844 +tomorrow     ==> ERROR: KEYWORD\r
845               ==> Tue Jan 11 11:11:00 +0000 2011 # empty string is reference time\r
846  \r
847  Aug 3 23:06:06 2012             ==> Fri Aug 03 23:06:06 +0000 2012 # date(1) default format without TZ code\r
848 @@ -52,13 +57,15 @@ Fri, 03 Aug 2012 23:07:46 +0100 ==> Fri Aug 03 22:07:46 +0000 2012 # rfc-2822\r
849  \r
850  19701223 +0100 ==> Wed Dec 23 11:11:00 +0000 1970 # Timezone is ignored without an error\r
851  \r
852 +today ==^^> Wed Jan 12 00:00:00 +0000 2011\r
853  today ==^> Tue Jan 11 23:59:59 +0000 2011\r
854  today ==_> Tue Jan 11 00:00:00 +0000 2011\r
855  \r
856 -thisweek ==^> Sat Jan 15 23:59:59 +0000 2011\r
857 -thisweek ==_> Sun Jan 09 00:00:00 +0000 2011\r
858 +this week ==^^> Sun Jan 16 00:00:00 +0000 2011\r
859 +this week ==^> Sat Jan 15 23:59:59 +0000 2011\r
860 +this week ==_> Sun Jan 09 00:00:00 +0000 2011\r
861  \r
862 -two months ago==> ERROR: 1 # "ago" is not supported\r
863 +two months ago ==> Thu Nov 11 11:11:00 +0000 2010\r
864  two months ==> Thu Nov 11 11:11:00 +0000 2010\r
865  \r
866  @1348569850 ==> Tue Sep 25 10:44:10 +0000 2012\r
867 diff --git a/test/parse-time.c b/test/parse-time.c\r
868 index 5f73b85..901a4dd 100644\r
869 --- a/test/parse-time.c\r
870 +++ b/test/parse-time.c\r
871 @@ -29,6 +29,28 @@\r
872  \r
873  #define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))\r
874  \r
875 +static const char *parse_time_error_strings[] = {\r
876 +    [PARSE_TIME_OK]                    = "OK",\r
877 +    [PARSE_TIME_ERR]                   = "ERR",\r
878 +    [PARSE_TIME_ERR_LIB]               = "LIB",\r
879 +    [PARSE_TIME_ERR_ALREADYSET]                = "ALREADYSET",\r
880 +    [PARSE_TIME_ERR_FORMAT]            = "FORMAT",\r
881 +    [PARSE_TIME_ERR_DATEFORMAT]                = "DATEFORMAT",\r
882 +    [PARSE_TIME_ERR_TIMEFORMAT]                = "TIMEFORMAT",\r
883 +    [PARSE_TIME_ERR_INVALIDDATE]       = "INVALIDDATE",\r
884 +    [PARSE_TIME_ERR_INVALIDTIME]       = "INVALIDTIME",\r
885 +    [PARSE_TIME_ERR_KEYWORD]           = "KEYWORD",\r
886 +};\r
887 +\r
888 +static const char *\r
889 +parse_time_strerror (unsigned int errnum)\r
890 +{\r
891 +    if (errnum < ARRAY_SIZE (parse_time_error_strings))\r
892 +       return parse_time_error_strings[errnum];\r
893 +    else\r
894 +       return NULL;\r
895 +}\r
896 +\r
897  /*\r
898   * concat argv[start]...argv[end - 1], separating them by a single\r
899   * space, to a malloced string\r
900 @@ -188,7 +210,11 @@ parse_stdin (FILE *infile, time_t *ref, int round, const char *format)\r
901  \r
902             strftime (result, sizeof (result), format, &tm);\r
903         } else {\r
904 -           snprintf (result, sizeof (result), "ERROR: %d", r);\r
905 +           const char *errstr = parse_time_strerror (r);\r
906 +           if (errstr)\r
907 +               snprintf (result, sizeof (result), "ERROR: %s", errstr);\r
908 +           else\r
909 +               snprintf (result, sizeof (result), "ERROR: %d", r);\r
910         }\r
911  \r
912         printf ("%s%s %s%s", input, oper, result, trail);\r
913 @@ -268,8 +294,15 @@ main (int argc, char *argv[])\r
914  \r
915      free (argstr);\r
916  \r
917 -    if (r)\r
918 -       return 1;\r
919 +    if (r) {\r
920 +       const char *errstr = parse_time_strerror (r);\r
921 +       if (errstr)\r
922 +           fprintf (stderr, "ERROR: %s\n", errstr);\r
923 +       else\r
924 +           fprintf (stderr, "ERROR: %d\n", r);\r
925 +\r
926 +       return r;\r
927 +    }\r
928  \r
929      if (!localtime_r (&result, &tm))\r
930         return 1;\r