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
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
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
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
48 Content-Type: text/plain; charset=UTF-8
\r
49 Content-Transfer-Encoding: 8bit
\r
51 ALoCoQkQRcUI4et7w5DDMQLuSefLUl2yNxad5jM+csYUyuBkH/wF3H7LeDTBlIFm83LsnC5E7RZ5
\r
52 X-BeenThere: notmuch@notmuchmail.org
\r
53 X-Mailman-Version: 2.1.13
\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
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
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
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
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
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
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
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
112 Makefile.local | 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
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
147 diff between v5 and v6:
\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
154 +/* parse-time-vrp.cc - date range query glue
\r
156 + * This file is part of notmuch.
\r
158 + * Copyright © 2012 Jani Nikula
\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
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
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
173 + * Author: Jani Nikula <jani@nikula.org>
\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
183 +/* parse-time-vrp.h - date range query glue
\r
185 + * This file is part of notmuch.
\r
187 + * Copyright © 2012 Jani Nikula
\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
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
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
202 + * Author: Jani Nikula <jani@nikula.org>
\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
213 .SH DATE AND TIME SEARCH
\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
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
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
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
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
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
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
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
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
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
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
286 H[H] (am|a.m.|pm|p.m.)
\r
287 @@ -219,7 +222,7 @@ Examples: 17:05, 5pm
\r
291 -.B Supported date formats
\r
292 +.B Supported absolute date formats
\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
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
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
313 -/* Get epoch value for field. */
\r
314 +/* Get the smallest acceptable value for field. */
\r
316 -field_epoch (enum field field)
\r
317 +get_field_epoch_value (enum field field)
\r
319 if (field == TM_ABS_MDAY || field == TM_ABS_MON)
\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
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
333 if (!state->postponed_length)
\r
335 @@ -279,8 +280,7 @@ is_field_set (struct state *state, enum field field)
\r
337 assert (field < ARRAY_SIZE (state->tm));
\r
339 - return field < ARRAY_SIZE (state->set) &&
\r
340 - state->set[field] != FIELD_UNSET;
\r
341 + return state->set[field] != FIELD_UNSET;
\r
345 @@ -301,10 +301,8 @@ set_field (struct state *state, enum field field, int value)
\r
349 - assert (field < ARRAY_SIZE (state->tm));
\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
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
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
366 - assert (field < ARRAY_SIZE (state->tm)); /* assert relative??? */
\r
367 + assert (field < ARRAY_SIZE (state->tm));
\r
369 - if (field < ARRAY_SIZE (state->set))
\r
370 - state->set[field] = FIELD_SET;
\r
371 + state->set[field] = FIELD_SET;
\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
377 static bool is_valid_12hour (int h)
\r
379 - return h >= 0 && h <= 12;
\r
380 + return h >= 1 && h <= 12;
\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
388 -kw_set_default (struct state *state, struct keyword *kw)
\r
390 - return set_field (state, kw->field, kw->value);
\r
394 kw_set_rel (struct state *state, struct keyword *kw)
\r
396 int multiplier = 1;
\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
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
408 @@ -521,7 +512,7 @@ kw_set_month (struct state *state, struct keyword *kw)
\r
409 if (n == 1 || n == 2) {
\r
412 - get_postponed_number (state, &v, NULL, NULL);
\r
413 + consume_postponed_number (state, &v, NULL, NULL);
\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
421 - get_postponed_number (state, &v, NULL, NULL);
\r
422 + consume_postponed_number (state, &v, NULL, NULL);
\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
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
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
440 +kw_ignore (unused (struct state *state), unused (struct keyword *kw))
\r
446 * Accepted keywords.
\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
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
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
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
467 + * Note: Observe how "m" and "mi" match minutes, "M" and "mo" and
\r
468 + * "mont" match months, but "mon" matches Monday.
\r
470 static struct keyword keywords[] = {
\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
488 { N_("jan|uary"), TM_ABS_MON, 1, kw_set_month },
\r
489 @@ -648,15 +645,15 @@ static struct keyword keywords[] = {
\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
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
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
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
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
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
538 + * Excessive, consecutive, and misplaced (at the beginning or end) '|'
\r
539 + * characters in keyword are handled gracefully. Only the first one
\r
542 + * If match_case is true, the matching is case sensitive.
\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
549 + const char *s = str;
\r
550 + bool prefix_matched = false;
\r
555 + while (*keyword == '|') {
\r
556 + prefix_matched = true;
\r
560 + if (!*s || !isalpha ((unsigned char) *s) || !*keyword)
\r
563 - for (i = 0; *s && *keyword; i++, s++, keyword++) {
\r
565 if (*s != *keyword)
\r
569 if (tolower ((unsigned char) *s) !=
\r
570 tolower ((unsigned char) *keyword))
\r
579 - return i < n ? 0 : i;
\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
586 + /* did not match enough of keyword */
\r
587 + if (*keyword && !prefix_matched)
\r
594 @@ -753,36 +778,24 @@ static ssize_t
\r
595 parse_keyword (struct state *state, const char *s)
\r
598 - size_t n, max_n = 0;
\r
600 struct keyword *kw = NULL;
\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
611 - strncpy (keyword, _(keywords[i].name), sizeof (keyword));
\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
618 - /* Minimum match length. */
\r
619 - p = strchr (keyword, '|');
\r
621 - minlen = p - keyword;
\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
631 - n = match_keyword (s, keyword, minlen, mcase);
\r
632 - if (n > max_n || (n == max_n && mcase)) {
\r
634 + n = match_keyword (s, keyword, mcase);
\r
641 @@ -792,12 +805,12 @@ parse_keyword (struct state *state, const char *s)
\r
643 r = kw->set (state, kw);
\r
645 - r = kw_set_default (state, kw);
\r
646 + r = set_field (state, kw->field, kw->value);
\r
656 @@ -832,7 +845,7 @@ parse_postponed_number (struct state *state, unused (enum field next_field))
\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
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
668 return set_abs_date (state, year, mon, mday);
\r
670 - return -PARSE_TIME_ERR_FORMAT;
\r
673 return -PARSE_TIME_ERR_FORMAT;
\r
674 @@ -1100,10 +1111,7 @@ parse_number (struct state *state, const char *s)
\r
676 v1 = strtoul_len (p, &p, &n1);
\r
678 - if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {
\r
680 - v2 = strtoul_len (p + 1, &p, &n2);
\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
686 @@ -1112,6 +1120,9 @@ parse_number (struct state *state, const char *s)
\r
691 + v2 = strtoul_len (p + 1, &p, &n2);
\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
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
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
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
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
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
739 @@ -1294,7 +1310,7 @@ fixup_ampm (struct state *state)
\r
743 - mod_field (state, TM_REL_HOUR, -hdiff);
\r
744 + add_to_field (state, TM_REL_HOUR, -hdiff);
\r
748 @@ -1311,7 +1327,7 @@ create_output (struct state *state, time_t *t_out, const time_t *ref,
\r
750 int week_round = PARSE_TIME_NO_ROUND;
\r
752 - r = initialize_now (state, &now, ref);
\r
753 + r = initialize_now (state, ref, &now);
\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
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
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
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
778 - unset_field (state, TM_ABS_WDAY);
\r
779 + unset_field (state, TM_WDAY);
\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
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
789 + * This is the most accurate field
\r
790 + * specified. Round up adjusting it towards
\r
793 + add_to_field (state, r, -1);
\r
796 + * Go back a second if the result is to be used
\r
797 + * for inclusive comparisons.
\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
803 round = PARSE_TIME_NO_ROUND; /* No more rounding. */
\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
809 - set_field (state, f, field_epoch (f));
\r
810 + set_field (state, f, get_field_epoch_value (f));
\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
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
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
850 19701223 +0100 ==> Wed Dec 23 11:11:00 +0000 1970 # Timezone is ignored without an error
\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
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
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
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
873 #define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
\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
888 +static const char *
\r
889 +parse_time_strerror (unsigned int errnum)
\r
891 + if (errnum < ARRAY_SIZE (parse_time_error_strings))
\r
892 + return parse_time_error_strings[errnum];
\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
902 strftime (result, sizeof (result), format, &tm);
\r
904 - snprintf (result, sizeof (result), "ERROR: %d", r);
\r
905 + const char *errstr = parse_time_strerror (r);
\r
907 + snprintf (result, sizeof (result), "ERROR: %s", errstr);
\r
909 + snprintf (result, sizeof (result), "ERROR: %d", r);
\r
912 printf ("%s%s %s%s", input, oper, result, trail);
\r
913 @@ -268,8 +294,15 @@ main (int argc, char *argv[])
\r
920 + const char *errstr = parse_time_strerror (r);
\r
922 + fprintf (stderr, "ERROR: %s\n", errstr);
\r
924 + fprintf (stderr, "ERROR: %d\n", r);
\r
929 if (!localtime_r (&result, &tm))
\r