Re: [PATCH v2] emacs: insert quotable parts in reply as they are displayed in show...
[notmuch-archives.git] / a1 / dd219d02814c7bd91c5603b385d26accb7c89f
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 69600431FC2\r
6         for <notmuch@notmuchmail.org>; Sun, 19 Feb 2012 14:56:09 -0800 (PST)\r
7 X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
8 X-Amavis-Alert: BAD HEADER SECTION, Duplicate header field: "References"\r
9 X-Spam-Flag: NO\r
10 X-Spam-Score: -0.7\r
11 X-Spam-Level: \r
12 X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5\r
13         tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled\r
14 Received: from olra.theworths.org ([127.0.0.1])\r
15         by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
16         with ESMTP id jSPVZhoQaww0 for <notmuch@notmuchmail.org>;\r
17         Sun, 19 Feb 2012 14:56:01 -0800 (PST)\r
18 Received: from mail-lpp01m010-f53.google.com (mail-lpp01m010-f53.google.com\r
19         [209.85.215.53]) (using TLSv1 with cipher RC4-SHA (128/128 bits))\r
20         (No client certificate requested)\r
21         by olra.theworths.org (Postfix) with ESMTPS id 8FACD431FAF\r
22         for <notmuch@notmuchmail.org>; Sun, 19 Feb 2012 14:56:00 -0800 (PST)\r
23 Received: by mail-lpp01m010-f53.google.com with SMTP id d3so5943891lah.26\r
24         for <notmuch@notmuchmail.org>; Sun, 19 Feb 2012 14:56:00 -0800 (PST)\r
25 Received-SPF: pass (google.com: domain of jani@nikula.org designates\r
26         10.152.127.136 as permitted sender) client-ip=10.152.127.136; \r
27 Authentication-Results: mr.google.com;\r
28         spf=pass (google.com: domain of jani@nikula.org\r
29         designates 10.152.127.136 as permitted sender)\r
30         smtp.mail=jani@nikula.org\r
31 Received: from mr.google.com ([10.152.127.136])\r
32         by 10.152.127.136 with SMTP id ng8mr13655022lab.16.1329692160207\r
33         (num_hops = 1); Sun, 19 Feb 2012 14:56:00 -0800 (PST)\r
34 Received: by 10.152.127.136 with SMTP id ng8mr11420481lab.16.1329692160131;\r
35         Sun, 19 Feb 2012 14:56:00 -0800 (PST)\r
36 Received: from localhost (dsl-hkibrasgw4-fe50f800-253.dhcp.inet.fi.\r
37         [84.248.80.253])\r
38         by mx.google.com with ESMTPS id w10sm21412303lbe.14.2012.02.19.14.55.57\r
39         (version=SSLv3 cipher=OTHER); Sun, 19 Feb 2012 14:55:59 -0800 (PST)\r
40 From: Jani Nikula <jani@nikula.org>\r
41 To: notmuch@notmuchmail.org\r
42 Subject: [RFC PATCH 1/2] lib: add date/time parser\r
43 Date: Mon, 20 Feb 2012 00:55:51 +0200\r
44 Message-Id:\r
45  <25ea278088dd81c51496825c4f8365796de29094.1329689945.git.jani@nikula.org>\r
46 X-Mailer: git-send-email 1.7.5.4\r
47 In-Reply-To: <cover.1329689945.git.jani@nikula.org>\r
48 References: <cover.1329689945.git.jani@nikula.org>\r
49 In-Reply-To: <cover.1329689945.git.jani@nikula.org>\r
50 References: <cover.1329689945.git.jani@nikula.org>\r
51 MIME-Version: 1.0\r
52 Content-Type: text/plain; charset=UTF-8\r
53 Content-Transfer-Encoding: 8bit\r
54 X-Gm-Message-State:\r
55  ALoCoQlxnB1PStZBQKPPefNV0WjSZ39EwlJf1HVTA3biy+mvHge/BE939zs3RRF1bm+12NQYdCX0\r
56 X-BeenThere: notmuch@notmuchmail.org\r
57 X-Mailman-Version: 2.1.13\r
58 Precedence: list\r
59 List-Id: "Use and development of the notmuch mail system."\r
60         <notmuch.notmuchmail.org>\r
61 List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
62         <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
63 List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
64 List-Post: <mailto:notmuch@notmuchmail.org>\r
65 List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
66 List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
67         <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
68 X-List-Received-Date: Sun, 19 Feb 2012 22:56:10 -0000\r
69 \r
70 Signed-off-by: Jani Nikula <jani@nikula.org>\r
71 ---\r
72  lib/Makefile.local      |    1 +\r
73  lib/parse-time-string.c | 1304 +++++++++++++++++++++++++++++++++++++++++++++++\r
74  lib/parse-time-string.h |   95 ++++\r
75  3 files changed, 1400 insertions(+), 0 deletions(-)\r
76  create mode 100644 lib/parse-time-string.c\r
77  create mode 100644 lib/parse-time-string.h\r
78 \r
79 diff --git a/lib/Makefile.local b/lib/Makefile.local\r
80 index 54c4dea..803a284 100644\r
81 --- a/lib/Makefile.local\r
82 +++ b/lib/Makefile.local\r
83 @@ -53,6 +53,7 @@ libnotmuch_c_srcs =           \\r
84         $(dir)/libsha1.c        \\r
85         $(dir)/message-file.c   \\r
86         $(dir)/messages.c       \\r
87 +       $(dir)/parse-time-string.c      \\r
88         $(dir)/sha1.c           \\r
89         $(dir)/tags.c\r
90  \r
91 diff --git a/lib/parse-time-string.c b/lib/parse-time-string.c\r
92 new file mode 100644\r
93 index 0000000..59713dc\r
94 --- /dev/null\r
95 +++ b/lib/parse-time-string.c\r
96 @@ -0,0 +1,1304 @@\r
97 +/*\r
98 + * parse time string - user friendly date and time parser\r
99 + * Copyright © 2012 Jani Nikula\r
100 + *\r
101 + * This program is free software: you can redistribute it and/or modify\r
102 + * it under the terms of the GNU General Public License as published by\r
103 + * the Free Software Foundation, either version 2 of the License, or\r
104 + * (at your option) any later version.\r
105 + *\r
106 + * This program is distributed in the hope that it will be useful,\r
107 + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
108 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
109 + * GNU General Public License for more details.\r
110 + *\r
111 + * You should have received a copy of the GNU General Public License\r
112 + * along with this program.  If not, see <http://www.gnu.org/licenses/>.\r
113 + *\r
114 + * Author: Jani Nikula <jani@nikula.org>\r
115 + */\r
116 +\r
117 +#ifndef PARSE_TIME_DEBUG\r
118 +#define NDEBUG /* for assert() */\r
119 +#endif\r
120 +\r
121 +#include <assert.h>\r
122 +#include <ctype.h>\r
123 +#include <errno.h>\r
124 +#include <limits.h>\r
125 +#include <stdio.h>\r
126 +#include <stdarg.h>\r
127 +#include <stdbool.h>\r
128 +#include <stdlib.h>\r
129 +#include <string.h>\r
130 +#include <strings.h>\r
131 +#include <time.h>\r
132 +#include <sys/time.h>\r
133 +#include <sys/types.h>\r
134 +\r
135 +#include "parse-time-string.h"\r
136 +\r
137 +#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))\r
138 +\r
139 +/* field indices in struct state tm, and set fields */\r
140 +enum field {\r
141 +    /* keep SEC...YEAR in this order */\r
142 +    TM_ABS_SEC,                /* seconds */\r
143 +    TM_ABS_MIN,                /* minutes */\r
144 +    TM_ABS_HOUR,       /* hours */\r
145 +    TM_ABS_MDAY,       /* day of the month */\r
146 +    TM_ABS_MON,                /* month */\r
147 +    TM_ABS_YEAR,       /* year */\r
148 +\r
149 +    TM_ABS_WDAY,       /* day of the week. special: may be relative */\r
150 +    TM_ABS_ISDST,      /* daylight saving time */\r
151 +\r
152 +    TM_AMPM,           /* am vs. pm */\r
153 +    TM_TZ,             /* timezone in minutes */\r
154 +\r
155 +    /* keep SEC...YEAR in this order */\r
156 +    TM_REL_SEC,                /* seconds relative to now */\r
157 +    TM_REL_MIN,                /* minutes ... */\r
158 +    TM_REL_HOUR,       /* hours ... */\r
159 +    TM_REL_DAY,                /* days ... */\r
160 +    TM_REL_MON,                /* months ... */\r
161 +    TM_REL_YEAR,       /* years ... */\r
162 +    TM_REL_WEEK,       /* weeks ... */\r
163 +\r
164 +    TM_NONE,           /* not a field */\r
165 +\r
166 +    TM_SIZE = TM_NONE,\r
167 +};\r
168 +\r
169 +enum field_set {\r
170 +    FIELD_UNSET,\r
171 +    FIELD_SET,\r
172 +    FIELD_NOW,\r
173 +};\r
174 +\r
175 +static enum field\r
176 +next_field (enum field field)\r
177 +{\r
178 +    /* note: depends on the enum ordering */\r
179 +    return field < TM_ABS_YEAR ? field + 1 : TM_NONE;\r
180 +}\r
181 +\r
182 +static enum field\r
183 +abs_to_rel_field (enum field field)\r
184 +{\r
185 +    assert (field <= TM_ABS_YEAR);\r
186 +\r
187 +    /* note: depends on the enum ordering */\r
188 +    return field + (TM_REL_SEC - TM_ABS_SEC);\r
189 +}\r
190 +\r
191 +/* get zero value for field */\r
192 +static int\r
193 +field_zero (enum field field)\r
194 +{\r
195 +    if (field == TM_ABS_MDAY || field == TM_ABS_MON)\r
196 +       return 1;\r
197 +    else if (field == TM_ABS_YEAR)\r
198 +       return 1970;\r
199 +    else\r
200 +       return 0;\r
201 +}\r
202 +\r
203 +struct state {\r
204 +    int tm[TM_SIZE];                   /* parsed date and time */\r
205 +    enum field_set set[TM_SIZE];       /* set status of tm */\r
206 +\r
207 +    enum field last_field;\r
208 +    char delim;\r
209 +\r
210 +    int postponed_length;      /* number of digits in postponed value */\r
211 +    int postponed_value;\r
212 +};\r
213 +\r
214 +/*\r
215 + * Helpers for postponed numbers.\r
216 + *\r
217 + * postponed_length is the number of digits in postponed value. 0\r
218 + * means there is no postponed number. -1 means there is a postponed\r
219 + * number, but it comes from a keyword, and it doesn't have digits.\r
220 + */\r
221 +static int\r
222 +get_postponed_length (struct state *state)\r
223 +{\r
224 +    return state->postponed_length;\r
225 +}\r
226 +\r
227 +static bool\r
228 +get_postponed_number (struct state *state, int *v, int *n)\r
229 +{\r
230 +    if (!state->postponed_length)\r
231 +       return false;\r
232 +\r
233 +    if (n)\r
234 +       *n = state->postponed_length;\r
235 +\r
236 +    if (v)\r
237 +       *v = state->postponed_value;\r
238 +\r
239 +    state->postponed_length = 0;\r
240 +    state->postponed_value = 0;\r
241 +\r
242 +    return true;\r
243 +}\r
244 +\r
245 +/* parse postponed number if one exists */\r
246 +static int parse_postponed_number (struct state *state, int v, int n);\r
247 +static int\r
248 +handle_postponed_number (struct state *state)\r
249 +{\r
250 +    int v = state->postponed_value;\r
251 +    int n = state->postponed_length;\r
252 +\r
253 +    if (!n)\r
254 +       return 0;\r
255 +\r
256 +    state->postponed_value = 0;\r
257 +    state->postponed_length = 0;\r
258 +\r
259 +    return parse_postponed_number (state, v, n);\r
260 +}\r
261 +\r
262 +/*\r
263 + * set new postponed number to be handled later. if one exists\r
264 + * already, handle it first. n may be -1 to indicate a keyword that\r
265 + * has no number length.\r
266 + */\r
267 +static int\r
268 +set_postponed_number (struct state *state, int v, int n)\r
269 +{\r
270 +    int r;\r
271 +\r
272 +    /* parse previous postponed number, if any */\r
273 +    r = handle_postponed_number (state);\r
274 +    if (r)\r
275 +       return r;\r
276 +\r
277 +    state->postponed_length = n;\r
278 +    state->postponed_value = v;\r
279 +\r
280 +    return 0;\r
281 +}\r
282 +\r
283 +static void\r
284 +set_delim (struct state *state, char delim)\r
285 +{\r
286 +    state->delim = delim;\r
287 +}\r
288 +\r
289 +static void\r
290 +unset_delim (struct state *state)\r
291 +{\r
292 +    state->delim = 0;\r
293 +}\r
294 +\r
295 +/*\r
296 + * Field set/get/mod helpers.\r
297 + */\r
298 +\r
299 +/* returns unset for non-tracked fields */\r
300 +static bool\r
301 +is_field_set (struct state *state, enum field field)\r
302 +{\r
303 +    assert (field < ARRAY_SIZE (state->tm));\r
304 +\r
305 +    return field < ARRAY_SIZE (state->set) &&\r
306 +          state->set[field] != FIELD_UNSET;\r
307 +}\r
308 +\r
309 +static void\r
310 +unset_field (struct state *state, enum field field)\r
311 +{\r
312 +    assert (field < ARRAY_SIZE (state->tm));\r
313 +\r
314 +    state->set[field] = FIELD_UNSET;\r
315 +    state->tm[field] = 0;\r
316 +}\r
317 +\r
318 +/* Set field to value. */\r
319 +static int\r
320 +set_field (struct state *state, enum field field, int value)\r
321 +{\r
322 +    int r;\r
323 +\r
324 +    assert (field < ARRAY_SIZE (state->tm));\r
325 +\r
326 +    /* some fields can only be set once */\r
327 +    if (field < ARRAY_SIZE (state->set) && state->set[field] != FIELD_UNSET)\r
328 +       return -PARSE_TIME_ERR_ALREADYSET;\r
329 +\r
330 +    state->set[field] = FIELD_SET;\r
331 +\r
332 +    /*\r
333 +     * REVISIT: There could be a "next_field" that would be set from\r
334 +     * "field" for the duration of the handle_postponed_number() call,\r
335 +     * so it has more information to work with.\r
336 +     */\r
337 +\r
338 +    /* parse postponed number, if any */\r
339 +    r = handle_postponed_number (state);\r
340 +    if (r)\r
341 +       return r;\r
342 +\r
343 +    unset_delim (state);\r
344 +\r
345 +    state->tm[field] = value;\r
346 +    state->last_field = field;\r
347 +\r
348 +    return 0;\r
349 +}\r
350 +\r
351 +/*\r
352 + * Mark n fields in fields to be set to current date/time in the\r
353 + * specified time zone, or local timezone if not specified. The fields\r
354 + * will be initialized after parsing is complete and timezone is\r
355 + * known.\r
356 + */\r
357 +static int\r
358 +set_fields_to_now (struct state *state, enum field *fields, size_t n)\r
359 +{\r
360 +    size_t i;\r
361 +    int r;\r
362 +\r
363 +    for (i = 0; i < n; i++) {\r
364 +       r = set_field (state, fields[i], 0);\r
365 +       if (r)\r
366 +           return r;\r
367 +       state->set[fields[i]] = FIELD_NOW;\r
368 +    }\r
369 +\r
370 +    return 0;\r
371 +}\r
372 +\r
373 +/* Modify field by adding value to it. To be used on relative fields. */\r
374 +static int\r
375 +mod_field (struct state *state, enum field field, int value)\r
376 +{\r
377 +    int r;\r
378 +\r
379 +    assert (field < ARRAY_SIZE (state->tm));   /* assert relative??? */\r
380 +\r
381 +    if (field < ARRAY_SIZE (state->set))\r
382 +       state->set[field] = FIELD_SET;\r
383 +\r
384 +    /* parse postponed number, if any */\r
385 +    r = handle_postponed_number (state);\r
386 +    if (r)\r
387 +       return r;\r
388 +\r
389 +    unset_delim (state);\r
390 +\r
391 +    state->tm[field] += value;\r
392 +    state->last_field = field;\r
393 +\r
394 +    return 0;\r
395 +}\r
396 +\r
397 +/*\r
398 + * Get field value. Make sure the field is set before query. It's most\r
399 + * likely an error to call this while parsing (for example fields set\r
400 + * as FIELD_NOW will only be set to some value after parsing).\r
401 + */\r
402 +static int\r
403 +get_field (struct state *state, enum field field)\r
404 +{\r
405 +    assert (field < ARRAY_SIZE (state->tm));\r
406 +\r
407 +    return state->tm[field];\r
408 +}\r
409 +\r
410 +/* Unset indicator for time and date set helpers. */\r
411 +#define UNSET -1\r
412 +\r
413 +/* Time set helper. No input checking. Use UNSET (-1) to leave unset. */\r
414 +static int\r
415 +set_abs_time (struct state *state, int hour, int min, int sec)\r
416 +{\r
417 +    int r;\r
418 +\r
419 +    if (hour != UNSET) {\r
420 +       if ((r = set_field (state, TM_ABS_HOUR, hour)))\r
421 +           return r;\r
422 +    }\r
423 +\r
424 +    if (min != UNSET) {\r
425 +       if ((r = set_field (state, TM_ABS_MIN, min)))\r
426 +           return r;\r
427 +    }\r
428 +\r
429 +    if (sec != UNSET) {\r
430 +       if ((r = set_field (state, TM_ABS_SEC, sec)))\r
431 +           return r;\r
432 +    }\r
433 +\r
434 +    return 0;\r
435 +}\r
436 +\r
437 +/* Date set helper. No input checking. Use UNSET (-1) to leave unset. */\r
438 +static int\r
439 +set_abs_date (struct state *state, int year, int mon, int mday)\r
440 +{\r
441 +    int r;\r
442 +\r
443 +    if (year != UNSET) {\r
444 +       if ((r = set_field (state, TM_ABS_YEAR, year)))\r
445 +           return r;\r
446 +    }\r
447 +\r
448 +    if (mon != UNSET) {\r
449 +       if ((r = set_field (state, TM_ABS_MON, mon)))\r
450 +           return r;\r
451 +    }\r
452 +\r
453 +    if (mday != UNSET) {\r
454 +       if ((r = set_field (state, TM_ABS_MDAY, mday)))\r
455 +           return r;\r
456 +    }\r
457 +\r
458 +    return 0;\r
459 +}\r
460 +\r
461 +/*\r
462 + * Keyword parsing and handling.\r
463 + */\r
464 +struct keyword;\r
465 +typedef int (*setter_t)(struct state *state, struct keyword *kw);\r
466 +\r
467 +struct keyword {\r
468 +    const char *name;  /* keyword */\r
469 +    size_t minlen;     /* min length to match, 0 = must match all */\r
470 +    enum field field;  /* field to set, or FIELD_NONE if N/A */\r
471 +    int value;         /* value to set, or 0 if N/A */\r
472 +    setter_t set;      /* function to use for setting, if non-NULL */\r
473 +};\r
474 +\r
475 +/*\r
476 + * Setter callback functions for keywords.\r
477 + */\r
478 +static int\r
479 +kw_set_default (struct state *state, struct keyword *kw)\r
480 +{\r
481 +    return set_field (state, kw->field, kw->value);\r
482 +}\r
483 +\r
484 +static int\r
485 +kw_set_rel (struct state *state, struct keyword *kw)\r
486 +{\r
487 +    int multiplier = 1;\r
488 +\r
489 +    /* get a previously set multiplier, if any */\r
490 +    get_postponed_number (state, &multiplier, NULL);\r
491 +\r
492 +    /* accumulate relative field values */\r
493 +    return mod_field (state, kw->field, multiplier * kw->value);\r
494 +}\r
495 +\r
496 +static int\r
497 +kw_set_number (struct state *state, struct keyword *kw)\r
498 +{\r
499 +    /* -1 = no length, from keyword */\r
500 +    return set_postponed_number (state, kw->value, -1);\r
501 +}\r
502 +\r
503 +static int\r
504 +kw_set_month (struct state *state, struct keyword *kw)\r
505 +{\r
506 +    int n = get_postponed_length (state);\r
507 +\r
508 +    /* consume postponed number if it could be mday */\r
509 +    if (n == 1 || n == 2) {\r
510 +       int r, v;\r
511 +\r
512 +       get_postponed_number (state, &v, NULL);\r
513 +\r
514 +       if (v < 1 || v > 31)\r
515 +           return -PARSE_TIME_ERR_INVALIDDATE;\r
516 +\r
517 +       r = set_field (state, TM_ABS_MDAY, v);\r
518 +       if (r)\r
519 +           return r;\r
520 +    }\r
521 +\r
522 +    return set_field (state, kw->field, kw->value);\r
523 +}\r
524 +\r
525 +static int\r
526 +kw_set_ampm (struct state *state, struct keyword *kw)\r
527 +{\r
528 +    int n = get_postponed_length (state);\r
529 +\r
530 +    /* consume postponed number if it could be hour */\r
531 +    if (n == 1 || n == 2) {\r
532 +       int r, v;\r
533 +\r
534 +       get_postponed_number (state, &v, NULL);\r
535 +\r
536 +       if (v < 1 || v > 12)\r
537 +           return -PARSE_TIME_ERR_INVALIDTIME;\r
538 +\r
539 +       r = set_abs_time (state, v, 0, 0);\r
540 +       if (r)\r
541 +           return r;\r
542 +    }\r
543 +\r
544 +    return set_field (state, kw->field, kw->value);\r
545 +}\r
546 +\r
547 +static int\r
548 +kw_set_timeofday (struct state *state, struct keyword *kw)\r
549 +{\r
550 +    return set_abs_time (state, kw->value, 0, 0);\r
551 +}\r
552 +\r
553 +static int\r
554 +kw_set_today (struct state *state, struct keyword *kw)\r
555 +{\r
556 +    enum field fields[] = { TM_ABS_YEAR, TM_ABS_MON, TM_ABS_MDAY };\r
557 +\r
558 +    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));\r
559 +}\r
560 +\r
561 +static int\r
562 +kw_set_now (struct state *state, struct keyword *kw)\r
563 +{\r
564 +    enum field fields[] = { TM_ABS_HOUR, TM_ABS_MIN, TM_ABS_SEC };\r
565 +\r
566 +    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));\r
567 +}\r
568 +\r
569 +static int\r
570 +kw_set_ordinal (struct state *state, struct keyword *kw)\r
571 +{\r
572 +    int n, v;\r
573 +\r
574 +    /* require a postponed number */\r
575 +    if (!get_postponed_number (state, &v, &n))\r
576 +       return -PARSE_TIME_ERR_DATEFORMAT;\r
577 +\r
578 +    /* ordinals are mday */\r
579 +    if (n != 1 && n != 2)\r
580 +       return -PARSE_TIME_ERR_DATEFORMAT;\r
581 +\r
582 +    /* be strict about st, nd, rd, and lax about th */\r
583 +    if (strcasecmp (kw->name, "st") == 0 && v != 1 && v != 21 && v != 31)\r
584 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
585 +    else if (strcasecmp (kw->name, "nd") == 0 && v != 2 && v != 22)\r
586 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
587 +    else if (strcasecmp (kw->name, "rd") == 0 && v != 3 && v != 23)\r
588 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
589 +    else if (strcasecmp (kw->name, "th") == 0 && (v < 1 || v > 31))\r
590 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
591 +\r
592 +    return set_field (state, TM_ABS_MDAY, v);\r
593 +}\r
594 +\r
595 +/*\r
596 + * Accepted keywords.\r
597 + *\r
598 + * If keyword begins with upper case letter, then the matching will be\r
599 + * case sensitive. Otherwise the matching is case insensitive.\r
600 + *\r
601 + * If setter is NULL, set_default will be used.\r
602 + *\r
603 + * Note: Order matters. Matching is greedy, longest match is used, but\r
604 + * of equal length matches the first one is used.\r
605 + */\r
606 +static struct keyword keywords[] = {\r
607 +    /* weekdays */\r
608 +    { "sunday",                3,      TM_ABS_WDAY,    0,      NULL },\r
609 +    { "monday",                3,      TM_ABS_WDAY,    1,      NULL },\r
610 +    { "tuesday",       3,      TM_ABS_WDAY,    2,      NULL },\r
611 +    { "wednesday",     3,      TM_ABS_WDAY,    3,      NULL },\r
612 +    { "thursday",      3,      TM_ABS_WDAY,    4,      NULL },\r
613 +    { "friday",                3,      TM_ABS_WDAY,    5,      NULL },\r
614 +    { "saturday",      3,      TM_ABS_WDAY,    6,      NULL },\r
615 +\r
616 +    /* months */\r
617 +    { "january",       3,      TM_ABS_MON,     1,      kw_set_month },\r
618 +    { "february",      3,      TM_ABS_MON,     2,      kw_set_month },\r
619 +    { "march",         3,      TM_ABS_MON,     3,      kw_set_month },\r
620 +    { "april",         3,      TM_ABS_MON,     4,      kw_set_month },\r
621 +    { "may",           3,      TM_ABS_MON,     5,      kw_set_month },\r
622 +    { "june",          3,      TM_ABS_MON,     6,      kw_set_month },\r
623 +    { "july",          3,      TM_ABS_MON,     7,      kw_set_month },\r
624 +    { "august",                3,      TM_ABS_MON,     8,      kw_set_month },\r
625 +    { "september",     3,      TM_ABS_MON,     9,      kw_set_month },\r
626 +    { "october",       3,      TM_ABS_MON,     10,     kw_set_month },\r
627 +    { "november",      3,      TM_ABS_MON,     11,     kw_set_month },\r
628 +    { "december",      3,      TM_ABS_MON,     12,     kw_set_month },\r
629 +\r
630 +    /* durations */\r
631 +    { "years",         1,      TM_REL_YEAR,    1,      kw_set_rel },\r
632 +    { "weeks",         1,      TM_REL_WEEK,    1,      kw_set_rel },\r
633 +    { "days",          1,      TM_REL_DAY,     1,      kw_set_rel },\r
634 +    { "hours",         1,      TM_REL_HOUR,    1,      kw_set_rel },\r
635 +    { "hrs",           1,      TM_REL_HOUR,    1,      kw_set_rel },\r
636 +    /* M=months, m=minutes. single M must precede minutes in the list. */\r
637 +    { "M",             1,      TM_REL_MON,     1,      kw_set_rel },\r
638 +    { "minutes",       1,      TM_REL_MIN,     1,      kw_set_rel },\r
639 +    { "mins",          1,      TM_REL_MIN,     1,      kw_set_rel },\r
640 +    { "months",                1,      TM_REL_MON,     1,      kw_set_rel },\r
641 +    { "seconds",       1,      TM_REL_SEC,     1,      kw_set_rel },\r
642 +    { "secs",          1,      TM_REL_SEC,     1,      kw_set_rel },\r
643 +\r
644 +    /* numbers */\r
645 +    { "one",           0,      TM_NONE,        1,      kw_set_number },\r
646 +    { "two",           0,      TM_NONE,        2,      kw_set_number },\r
647 +    { "three",         0,      TM_NONE,        3,      kw_set_number },\r
648 +    { "four",          0,      TM_NONE,        4,      kw_set_number },\r
649 +    { "five",          0,      TM_NONE,        5,      kw_set_number },\r
650 +    { "six",           0,      TM_NONE,        6,      kw_set_number },\r
651 +    { "seven",         0,      TM_NONE,        7,      kw_set_number },\r
652 +    { "eight",         0,      TM_NONE,        8,      kw_set_number },\r
653 +    { "nine",          0,      TM_NONE,        9,      kw_set_number },\r
654 +    { "ten",           0,      TM_NONE,        10,     kw_set_number },\r
655 +    { "dozen",         0,      TM_NONE,        12,     kw_set_number },\r
656 +    { "hundred",       0,      TM_NONE,        100,    kw_set_number },\r
657 +\r
658 +    /* special number forms */\r
659 +    { "this",          0,      TM_NONE,        0,      kw_set_number },\r
660 +    { "last",          0,      TM_NONE,        1,      kw_set_number },\r
661 +\r
662 +    /* specials */\r
663 +    { "yesterday",     0,      TM_REL_DAY,     1,      kw_set_rel },\r
664 +    { "today",         0,      TM_NONE,        0,      kw_set_today },\r
665 +    { "now",           0,      TM_NONE,        0,      kw_set_now },\r
666 +    { "noon",          0,      TM_NONE,        12,     kw_set_timeofday },\r
667 +    { "midnight",      0,      TM_NONE,        0,      kw_set_timeofday },\r
668 +    { "am",            0,      TM_AMPM,        0,      kw_set_ampm },\r
669 +    { "a.m.",          0,      TM_AMPM,        0,      kw_set_ampm },\r
670 +    { "pm",            0,      TM_AMPM,        1,      kw_set_ampm },\r
671 +    { "p.m.",          0,      TM_AMPM,        1,      kw_set_ampm },\r
672 +    { "st",            0,      TM_NONE,        0,      kw_set_ordinal },\r
673 +    { "nd",            0,      TM_NONE,        0,      kw_set_ordinal },\r
674 +    { "rd",            0,      TM_NONE,        0,      kw_set_ordinal },\r
675 +    { "th",            0,      TM_NONE,        0,      kw_set_ordinal },\r
676 +\r
677 +    /* timezone codes: offset in minutes. FIXME: add more codes. */\r
678 +    { "pst",           0,      TM_TZ,          -8*60,  NULL },\r
679 +    { "mst",           0,      TM_TZ,          -7*60,  NULL },\r
680 +    { "cst",           0,      TM_TZ,          -6*60,  NULL },\r
681 +    { "est",           0,      TM_TZ,          -5*60,  NULL },\r
682 +    { "ast",           0,      TM_TZ,          -4*60,  NULL },\r
683 +    { "nst",           0,      TM_TZ,          -(3*60+30),     NULL },\r
684 +\r
685 +    { "gmt",           0,      TM_TZ,          0,      NULL },\r
686 +    { "utc",           0,      TM_TZ,          0,      NULL },\r
687 +\r
688 +    { "wet",           0,      TM_TZ,          0,      NULL },\r
689 +    { "cet",           0,      TM_TZ,          1*60,   NULL },\r
690 +    { "eet",           0,      TM_TZ,          2*60,   NULL },\r
691 +    { "fet",           0,      TM_TZ,          3*60,   NULL },\r
692 +\r
693 +    { "wat",           0,      TM_TZ,          1*60,   NULL },\r
694 +    { "cat",           0,      TM_TZ,          2*60,   NULL },\r
695 +    { "eat",           0,      TM_TZ,          3*60,   NULL },\r
696 +};\r
697 +\r
698 +/*\r
699 + * Compare strings s and keyword. Return number of matching chars on\r
700 + * match, 0 for no match. Match must be at least n chars (n == 0 all\r
701 + * of keyword), otherwise it's not a match. Use match_case for case\r
702 + * sensitive matching.\r
703 + */\r
704 +static size_t\r
705 +stringcmp (const char *s, const char *keyword, size_t n, bool match_case)\r
706 +{\r
707 +    size_t i;\r
708 +\r
709 +    for (i = 0; *s && *keyword; i++, s++, keyword++) {\r
710 +       if (match_case) {\r
711 +           if (*s != *keyword)\r
712 +               break;\r
713 +       } else {\r
714 +           if (tolower ((unsigned char) *s) !=\r
715 +               tolower ((unsigned char) *keyword))\r
716 +               break;\r
717 +       }\r
718 +    }\r
719 +\r
720 +    if (n)\r
721 +       return i < n ? 0 : i;\r
722 +    else\r
723 +       return *keyword ? 0 : i;\r
724 +}\r
725 +\r
726 +/*\r
727 + * Parse a keyword. Return < 0 on error, number of parsed chars on\r
728 + * success.\r
729 + */\r
730 +static ssize_t\r
731 +parse_keyword (struct state *state, const char *s)\r
732 +{\r
733 +    unsigned int i;\r
734 +    size_t n, max_n = 0;\r
735 +    struct keyword *kw = NULL;\r
736 +    int r;\r
737 +\r
738 +    /* Match longest keyword */\r
739 +    for (i = 0; i < ARRAY_SIZE (keywords); i++) {\r
740 +       /* Match case if keyword begins with upper case letter. */\r
741 +       bool mcase = isupper ((unsigned char) keywords[i].name[0]);\r
742 +\r
743 +       n = stringcmp (s, keywords[i].name, keywords[i].minlen, mcase);\r
744 +       if (n > max_n) {\r
745 +           max_n = n;\r
746 +           kw = &keywords[i];\r
747 +       }\r
748 +    }\r
749 +\r
750 +    if (!kw)\r
751 +       return -PARSE_TIME_ERR_KEYWORD;\r
752 +\r
753 +    if (kw->set)\r
754 +       r = kw->set (state, kw);\r
755 +    else\r
756 +       r = kw_set_default (state, kw);\r
757 +\r
758 +    return r < 0 ? r : max_n;\r
759 +}\r
760 +\r
761 +/*\r
762 + * Non-keyword parsers and their helpers.\r
763 + */\r
764 +\r
765 +static int\r
766 +set_user_tz (struct state *state, char sign, int hour, int min)\r
767 +{\r
768 +    int tz = hour * 60 + min;\r
769 +\r
770 +    assert (sign == '+' || sign == '-');\r
771 +\r
772 +    if (hour < 0 || hour > 14 || min < 0 || min > 60 || min % 15)\r
773 +       return -PARSE_TIME_ERR_INVALIDTIME;\r
774 +\r
775 +    if (sign == '-')\r
776 +       tz = -tz;\r
777 +\r
778 +    return set_field (state, TM_TZ, tz);\r
779 +}\r
780 +\r
781 +/*\r
782 + * Independent parsing of a postponed number when it wasn't consumed\r
783 + * during parsing of the following token.\r
784 + *\r
785 + * This should be able to trust that last_field and next_field are\r
786 + * right.\r
787 + */\r
788 +static int\r
789 +parse_postponed_number (struct state *state, int v, int n)\r
790 +{\r
791 +    /*\r
792 +     * alright, these are really lone, won't affect parsing of\r
793 +     * following items... it's not a multiplier, those have been eaten\r
794 +     * away.\r
795 +     *\r
796 +     * also note numbers eaten away by parse_single_number.\r
797 +     */\r
798 +\r
799 +    assert (n < 8);\r
800 +\r
801 +    switch (n) {\r
802 +    case 1:\r
803 +    case 2:\r
804 +       /* hour or mday or year */\r
805 +       if (state->last_field == TM_ABS_MON &&  /* FIXME: written mon! */\r
806 +           !is_field_set (state, TM_ABS_MDAY)) {\r
807 +           return set_field (state, TM_ABS_MDAY, v);\r
808 +       }\r
809 +       break;\r
810 +    case 4:\r
811 +       /* YYYY or +/-HHMM for TZ or HHMM or DDMM */\r
812 +       /* FIXME: state->delim is no longer right for this function!\r
813 +        * why not, it could be! */\r
814 +       if (!is_field_set (state, TM_ABS_YEAR)) {\r
815 +           /* FIXME: check year? */\r
816 +           return set_field (state, TM_ABS_YEAR, v);\r
817 +       }\r
818 +       break;\r
819 +    case 6:\r
820 +       /* FIXME: HHMMSS or DDMMYY */\r
821 +       break;\r
822 +    case -1:\r
823 +       /* REVISIT */\r
824 +       break;\r
825 +    case 3:\r
826 +    case 5:\r
827 +    case 7:\r
828 +    default:\r
829 +       break;\r
830 +    }\r
831 +\r
832 +    return -PARSE_TIME_ERR_FORMAT;\r
833 +}\r
834 +\r
835 +/* Parse a single number. Typically postpone parsing until later. */\r
836 +static int\r
837 +parse_single_number (struct state *state, unsigned long v,\r
838 +                    unsigned long n)\r
839 +{\r
840 +    assert (n);\r
841 +\r
842 +    /* parse things that can be parsed immediately */\r
843 +    if (n == 8) {\r
844 +       /* YYYYMMDD */\r
845 +       int year = v / 10000;\r
846 +       int mon = (v / 100) % 100;\r
847 +       int mday = v % 100;\r
848 +\r
849 +       if (year < 1970 || mon < 1 || mon > 12 || mday < 1 || mday > 31)\r
850 +           return -PARSE_TIME_ERR_INVALIDDATE;\r
851 +\r
852 +       return set_abs_date (state, year, mon, mday);\r
853 +    } else if (n > 8) {\r
854 +       /* FIXME: seconds since epoch */\r
855 +       return -PARSE_TIME_ERR_FORMAT;\r
856 +    }\r
857 +\r
858 +    if (v > INT_MAX)\r
859 +       return -PARSE_TIME_ERR_FORMAT;\r
860 +\r
861 +    return set_postponed_number (state, v, n);\r
862 +}\r
863 +\r
864 +static bool\r
865 +is_time_sep (char c)\r
866 +{\r
867 +    return c == ':';\r
868 +}\r
869 +\r
870 +static bool\r
871 +is_date_sep (char c)\r
872 +{\r
873 +    return c == '/' || c == '-' || c == '.';\r
874 +}\r
875 +\r
876 +static bool\r
877 +is_sep (char c)\r
878 +{\r
879 +    return is_time_sep (c) || is_date_sep (c);\r
880 +}\r
881 +\r
882 +/* two-digit year: 00...69 is 2000s, 70...99 1900s, if n == 0 keep unset */\r
883 +static int\r
884 +expand_year (unsigned long year, size_t n)\r
885 +{\r
886 +    if (n == 2) {\r
887 +       return (year < 70 ? 2000 : 1900) + year;\r
888 +    } else if (n == 4) {\r
889 +       return year;\r
890 +    } else {\r
891 +       return UNSET;\r
892 +    }\r
893 +}\r
894 +\r
895 +static int\r
896 +parse_date (struct state *state, char sep,\r
897 +           unsigned long v1, unsigned long v2, unsigned long v3,\r
898 +           size_t n1, size_t n2, size_t n3)\r
899 +{\r
900 +    int year = UNSET, mon = UNSET, mday = UNSET;\r
901 +\r
902 +    assert (is_date_sep (sep));\r
903 +\r
904 +    switch (sep) {\r
905 +    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */\r
906 +       if (n1 != 1 && n1 != 2)\r
907 +           return -PARSE_TIME_ERR_DATEFORMAT;\r
908 +\r
909 +       if ((n2 == 1 || n2 == 2) && (n3 == 0 || n3 == 2 || n3 == 4)) {\r
910 +           /* M[M]/D[D][/YY[YY]] */\r
911 +           year = expand_year (v3, n3);\r
912 +           mon = v1;\r
913 +           mday = v2;\r
914 +       } else if (n2 == 4 && n3 == 0) {\r
915 +           /* M[M]/YYYY */\r
916 +           year = v2;\r
917 +           mon = v1;\r
918 +       } else {\r
919 +           return -PARSE_TIME_ERR_DATEFORMAT;\r
920 +       }\r
921 +       break;\r
922 +\r
923 +    case '-': /* Date: YYYY-MM[-DD] or DD-MM[-YY[YY]] or MM-YYYY */\r
924 +       if (n1 == 4 && n2 == 2 && (n3 == 0 || n3 == 2)) {\r
925 +           /* YYYY-MM[-DD] */\r
926 +           year = v1;\r
927 +           mon = v2;\r
928 +           if (n3)\r
929 +               mday = v3;\r
930 +       } else if (n1 == 2 && n2 == 2 && (n3 == 0 || n3 == 2 || n3 == 4)) {\r
931 +           /* DD-MM[-YY[YY]] */\r
932 +           year = expand_year (v3, n3);\r
933 +           mon = v2;\r
934 +           mday = v1;\r
935 +       } else if (n1 == 2 && n2 == 4 && n3 == 0) {\r
936 +           /* MM-YYYY */\r
937 +           year = v2;\r
938 +           mon = v1;\r
939 +       } else {\r
940 +           return -PARSE_TIME_ERR_DATEFORMAT;\r
941 +       }\r
942 +       break;\r
943 +\r
944 +    case '.': /* Date: D[D].M[M][.[YY[YY]]] */\r
945 +       if ((n1 != 1 && n1 != 2) || (n2 != 1 && n2 != 2) ||\r
946 +           (n3 != 0 && n3 != 2 && n3 != 4))\r
947 +           return -PARSE_TIME_ERR_DATEFORMAT;\r
948 +\r
949 +       year = expand_year (v3, n3);\r
950 +       mon = v2;\r
951 +       mday = v1;\r
952 +       break;\r
953 +    }\r
954 +\r
955 +    if (year != UNSET && year < 1970)\r
956 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
957 +\r
958 +    if (mon != UNSET && (mon < 1 || mon > 12))\r
959 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
960 +\r
961 +    if (mday != UNSET && (mday < 1 || mday > 31))\r
962 +       return -PARSE_TIME_ERR_INVALIDDATE;\r
963 +\r
964 +    return set_abs_date (state, year, mon, mday);\r
965 +}\r
966 +\r
967 +static int\r
968 +parse_time (struct state *state, char sep,\r
969 +           unsigned long v1, unsigned long v2, unsigned long v3,\r
970 +           size_t n1, size_t n2, size_t n3)\r
971 +{\r
972 +    assert (is_time_sep (sep));\r
973 +\r
974 +    if ((n1 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 2))\r
975 +       return -PARSE_TIME_ERR_TIMEFORMAT;\r
976 +\r
977 +    /*\r
978 +     * REVISIT: this means it's required to set time *before* being\r
979 +     * able to set timezone\r
980 +     */\r
981 +    if (is_field_set (state, TM_ABS_HOUR) &&\r
982 +       is_field_set (state, TM_ABS_MIN) &&\r
983 +       n1 == 2 && n2 == 2 && n3 == 0 &&\r
984 +       (state->delim == '+' || state->delim == '-')) {\r
985 +       return set_user_tz (state, state->delim, v1, v2);\r
986 +    }\r
987 +\r
988 +    if (v1 > 24 || v2 > 60 || v3 > 60)\r
989 +       return -PARSE_TIME_ERR_INVALIDTIME;\r
990 +\r
991 +    return set_abs_time (state, v1, v2, n3 ? v3 : 0);\r
992 +}\r
993 +\r
994 +/* strtoul helper that assigns length */\r
995 +static unsigned long\r
996 +strtoul_len (const char *s, const char **endp, size_t *len)\r
997 +{\r
998 +    unsigned long val = strtoul (s, (char **) endp, 10);\r
999 +\r
1000 +    *len = *endp - s;\r
1001 +    return val;\r
1002 +}\r
1003 +\r
1004 +/*\r
1005 + * Parse a (group of) number(s). Return < 0 on error, number of parsed\r
1006 + * chars on success.\r
1007 + */\r
1008 +static ssize_t\r
1009 +parse_number (struct state *state, const char *s)\r
1010 +{\r
1011 +    int r;\r
1012 +    unsigned long v1, v2, v3 = 0;\r
1013 +    size_t n1, n2, n3 = 0;\r
1014 +    const char *p = s;\r
1015 +    char sep;\r
1016 +\r
1017 +    v1 = strtoul_len (p, &p, &n1);\r
1018 +\r
1019 +    if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {\r
1020 +       sep = *p;\r
1021 +       v2 = strtoul_len (p + 1, &p, &n2);\r
1022 +    } else {\r
1023 +       /* a single number */\r
1024 +       r = parse_single_number (state, v1, n1);\r
1025 +       if (r)\r
1026 +           return r;\r
1027 +\r
1028 +       return p - s;\r
1029 +    }\r
1030 +\r
1031 +    /* a group of two or three numbers? */\r
1032 +    if (*p == sep && isdigit ((unsigned char) *(p + 1)))\r
1033 +       v3 = strtoul_len (p + 1, &p, &n3);\r
1034 +\r
1035 +    if (is_time_sep (sep))\r
1036 +       r = parse_time (state, sep, v1, v2, v3, n1, n2, n3);\r
1037 +    else\r
1038 +       r = parse_date (state, sep, v1, v2, v3, n1, n2, n3);\r
1039 +\r
1040 +    if (r)\r
1041 +       return r;\r
1042 +\r
1043 +    return p - s;\r
1044 +}\r
1045 +\r
1046 +/*\r
1047 + * Parse delimiter(s). Return < 0 on error, number of parsed chars on\r
1048 + * success.\r
1049 + */\r
1050 +static ssize_t\r
1051 +parse_delim (struct state *state, const char *s)\r
1052 +{\r
1053 +    const char *p = s;\r
1054 +\r
1055 +    /*\r
1056 +     * REVISIT: any actions depending on the first delim after last\r
1057 +     * field? what could it be?\r
1058 +     */\r
1059 +\r
1060 +    /*\r
1061 +     * skip non-alpha and non-digit, and store the last for further\r
1062 +     * processing\r
1063 +     */\r
1064 +    while (*p && !isalnum ((unsigned char) *p)) {\r
1065 +       set_delim (state, *p);\r
1066 +       p++;\r
1067 +    }\r
1068 +\r
1069 +    return p - s;\r
1070 +}\r
1071 +\r
1072 +/*\r
1073 + * Parse a date/time string. Return < 0 on error, number of parsed\r
1074 + * chars on success.\r
1075 + */\r
1076 +static ssize_t\r
1077 +parse_input (struct state *state, const char *s)\r
1078 +{\r
1079 +    const char *p = s;\r
1080 +    ssize_t n;\r
1081 +    int r;\r
1082 +\r
1083 +    while (*p) {\r
1084 +       if (isalpha ((unsigned char) *p)) {\r
1085 +           n = parse_keyword (state, p);\r
1086 +       } else if (isdigit ((unsigned char) *p)) {\r
1087 +           n = parse_number (state, p);\r
1088 +       } else {\r
1089 +           n = parse_delim (state, p);\r
1090 +       }\r
1091 +\r
1092 +       if (n <= 0) {\r
1093 +           if (n == 0)\r
1094 +               n = -PARSE_TIME_ERR;\r
1095 +\r
1096 +           return n;             /* FIXME */\r
1097 +       }\r
1098 +\r
1099 +       p += n;\r
1100 +    }\r
1101 +\r
1102 +    /* parse postponed number, if any */\r
1103 +    r = handle_postponed_number (state);\r
1104 +    if (r < 0)\r
1105 +       return r;\r
1106 +\r
1107 +    return p - s;\r
1108 +}\r
1109 +\r
1110 +/*\r
1111 + * Processing the parsed input.\r
1112 + */\r
1113 +\r
1114 +/*\r
1115 + * Initialize reference time to tm. Use time zone in state if\r
1116 + * specified, otherwise local time. Use now for reference time if\r
1117 + * non-NULL, otherwise current time.\r
1118 + */\r
1119 +static int\r
1120 +initialize_now (struct state *state, struct tm *tm, const time_t *now)\r
1121 +{\r
1122 +    time_t t;\r
1123 +\r
1124 +    if (now) {\r
1125 +       t = *now;\r
1126 +    } else {\r
1127 +       if (time (&t) == (time_t) -1)\r
1128 +           return -PARSE_TIME_ERR_LIB;\r
1129 +    }\r
1130 +\r
1131 +    if (is_field_set (state, TM_TZ)) {\r
1132 +       /* some other time zone */\r
1133 +\r
1134 +       /* adjust now according to the TZ */\r
1135 +       t += get_field (state, TM_TZ) * 60;\r
1136 +\r
1137 +       /* it's not gm, but this doesn't mess with the tz */\r
1138 +       if (gmtime_r (&t, tm) == NULL)\r
1139 +           return -PARSE_TIME_ERR_LIB;\r
1140 +    } else {\r
1141 +       /* local time */\r
1142 +       if (localtime_r (&t, tm) == NULL)\r
1143 +           return -PARSE_TIME_ERR_LIB;\r
1144 +    }\r
1145 +\r
1146 +    return 0;\r
1147 +}\r
1148 +\r
1149 +/*\r
1150 + * Normalize tm according to mktime(3). Both mktime(3) and\r
1151 + * localtime_r(3) use local time, but they cancel each other out here,\r
1152 + * making this function agnostic to time zone.\r
1153 + */\r
1154 +static int\r
1155 +normalize_tm (struct tm *tm)\r
1156 +{\r
1157 +    time_t t = mktime (tm);\r
1158 +\r
1159 +    if (t == (time_t) -1)\r
1160 +       return -PARSE_TIME_ERR_LIB;\r
1161 +\r
1162 +    if (!localtime_r (&t, tm))\r
1163 +       return -PARSE_TIME_ERR_LIB;\r
1164 +\r
1165 +    return 0;\r
1166 +}\r
1167 +\r
1168 +/* Get field out of a struct tm. */\r
1169 +static int\r
1170 +tm_get_field (const struct tm *tm, enum field field)\r
1171 +{\r
1172 +    switch (field) {\r
1173 +    case TM_ABS_SEC:   return tm->tm_sec;\r
1174 +    case TM_ABS_MIN:   return tm->tm_min;\r
1175 +    case TM_ABS_HOUR:  return tm->tm_hour;\r
1176 +    case TM_ABS_MDAY:  return tm->tm_mday;\r
1177 +    case TM_ABS_MON:   return tm->tm_mon + 1; /* 0- to 1-based */\r
1178 +    case TM_ABS_YEAR:  return 1900 + tm->tm_year;\r
1179 +    case TM_ABS_WDAY:  return tm->tm_wday;\r
1180 +    case TM_ABS_ISDST: return tm->tm_isdst;\r
1181 +    default:\r
1182 +       assert (false);\r
1183 +       break;\r
1184 +    }\r
1185 +\r
1186 +    return 0;\r
1187 +}\r
1188 +\r
1189 +/* Modify hour according to am/pm setting. */\r
1190 +static int\r
1191 +fixup_ampm (struct state *state)\r
1192 +{\r
1193 +    int hour, hdiff = 0;\r
1194 +\r
1195 +    if (!is_field_set (state, TM_AMPM))\r
1196 +       return 0;\r
1197 +\r
1198 +    if (!is_field_set (state, TM_ABS_HOUR))\r
1199 +       return -PARSE_TIME_ERR_TIMEFORMAT;\r
1200 +\r
1201 +    hour = get_field (state, TM_ABS_HOUR);\r
1202 +    if (hour < 1 || hour > 12)\r
1203 +       return -PARSE_TIME_ERR_INVALIDTIME;\r
1204 +\r
1205 +    if (get_field (state, TM_AMPM)) {\r
1206 +       /* 12pm is noon */\r
1207 +       if (hour != 12)\r
1208 +           hdiff = 12;\r
1209 +    } else {\r
1210 +       /* 12am is midnight, beginning of day */\r
1211 +       if (hour == 12)\r
1212 +           hdiff = -12;\r
1213 +    }\r
1214 +\r
1215 +    mod_field (state, TM_REL_HOUR, -hdiff);\r
1216 +\r
1217 +    return 0;\r
1218 +}\r
1219 +\r
1220 +/* Combine absolute and relative fields, and round. */\r
1221 +static int\r
1222 +create_output (struct state *state, time_t *t_out, const time_t *tnow,\r
1223 +              int round)\r
1224 +{\r
1225 +    struct tm tm = { 0 };\r
1226 +    struct tm now;\r
1227 +    enum field f;\r
1228 +    int r;\r
1229 +    int week_round = PARSE_TIME_NO_ROUND;\r
1230 +\r
1231 +    r = initialize_now (state, &now, tnow);\r
1232 +    if (r)\r
1233 +       return r;\r
1234 +\r
1235 +    /* initialize uninitialized fields to now */\r
1236 +    for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {\r
1237 +       if (state->set[f] == FIELD_NOW) {\r
1238 +           state->tm[f] = tm_get_field (&now, f);\r
1239 +           state->set[f] = FIELD_SET;\r
1240 +       }\r
1241 +    }\r
1242 +\r
1243 +    /*\r
1244 +     * If MON is set but YEAR is not, refer to past month.\r
1245 +     *\r
1246 +     * REVISIT: Why are month/week special in this regard? What about\r
1247 +     * mday, or time. Should refer to past.\r
1248 +     */\r
1249 +    if (is_field_set (state, TM_ABS_MON) &&\r
1250 +       !is_field_set (state, TM_ABS_YEAR)) {\r
1251 +       if (get_field (state, TM_ABS_MON) >= tm_get_field (&now, TM_ABS_MON))\r
1252 +           mod_field (state, TM_REL_YEAR, 1);\r
1253 +    }\r
1254 +\r
1255 +    /*\r
1256 +     * If WDAY is set but MDAY is not, we consider WDAY relative\r
1257 +     *\r
1258 +     * REVISIT: This fails on stuff like "two months ago monday"\r
1259 +     * because two months ago wasn't the same day as today. Postpone\r
1260 +     * until we know date?\r
1261 +     */\r
1262 +    if (is_field_set (state, TM_ABS_WDAY) &&\r
1263 +       !is_field_set (state, TM_ABS_MDAY)) {\r
1264 +       int wday = get_field (state, TM_ABS_WDAY);\r
1265 +       int today = tm_get_field (&now, TM_ABS_WDAY);\r
1266 +       int rel_days;\r
1267 +\r
1268 +       if (today > wday)\r
1269 +           rel_days = today - wday;\r
1270 +       else\r
1271 +           rel_days = today + 7 - wday;\r
1272 +\r
1273 +       /* this also prevents special week rounding from happening */\r
1274 +       mod_field (state, TM_REL_DAY, rel_days);\r
1275 +\r
1276 +       unset_field (state, TM_ABS_WDAY);\r
1277 +    }\r
1278 +\r
1279 +    r = fixup_ampm (state);\r
1280 +    if (r)\r
1281 +       return r;\r
1282 +\r
1283 +    /*\r
1284 +     * Iterate fields from least accurate to most accurate, and set\r
1285 +     * unset fields according to requested rounding.\r
1286 +     */\r
1287 +    for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {\r
1288 +       if (round != PARSE_TIME_NO_ROUND) {\r
1289 +           enum field r = abs_to_rel_field (f);\r
1290 +\r
1291 +           if (is_field_set (state, f) || is_field_set (state, r)) {\r
1292 +               if (round >= PARSE_TIME_ROUND_UP)\r
1293 +                   mod_field (state, r, -1);\r
1294 +               round = PARSE_TIME_NO_ROUND; /* no more rounding */\r
1295 +           } else {\r
1296 +               if (f == TM_ABS_MDAY &&\r
1297 +                   is_field_set (state, TM_REL_WEEK)) {\r
1298 +                   /* week is most accurate */\r
1299 +                   week_round = round;\r
1300 +                   round = PARSE_TIME_NO_ROUND;\r
1301 +               } else {\r
1302 +                   set_field (state, f, field_zero (f));\r
1303 +               }\r
1304 +           }\r
1305 +       }\r
1306 +\r
1307 +       if (!is_field_set (state, f))\r
1308 +           set_field (state, f, tm_get_field (&now, f));\r
1309 +    }\r
1310 +\r
1311 +    /* special case: rounding with week accuracy */\r
1312 +    if (week_round != PARSE_TIME_NO_ROUND) {\r
1313 +       /* temporarily set more accurate fields to now */\r
1314 +       set_field (state, TM_ABS_SEC, tm_get_field (&now, TM_ABS_SEC));\r
1315 +       set_field (state, TM_ABS_MIN, tm_get_field (&now, TM_ABS_MIN));\r
1316 +       set_field (state, TM_ABS_HOUR, tm_get_field (&now, TM_ABS_HOUR));\r
1317 +       set_field (state, TM_ABS_MDAY, tm_get_field (&now, TM_ABS_MDAY));\r
1318 +    }\r
1319 +\r
1320 +    /*\r
1321 +     * set all fields. they may contain out of range values before\r
1322 +     * normalization by mktime(3).\r
1323 +     */\r
1324 +    tm.tm_sec = get_field (state, TM_ABS_SEC) - get_field (state, TM_REL_SEC);\r
1325 +    tm.tm_min = get_field (state, TM_ABS_MIN) - get_field (state, TM_REL_MIN);\r
1326 +    tm.tm_hour = get_field (state, TM_ABS_HOUR) - get_field (state, TM_REL_HOUR);\r
1327 +    tm.tm_mday = get_field (state, TM_ABS_MDAY) -\r
1328 +                get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK);\r
1329 +    tm.tm_mon = get_field (state, TM_ABS_MON) - get_field (state, TM_REL_MON);\r
1330 +    tm.tm_mon--; /* 1- to 0-based */\r
1331 +    tm.tm_year = get_field (state, TM_ABS_YEAR) - get_field (state, TM_REL_YEAR) - 1900;\r
1332 +\r
1333 +    /*\r
1334 +     * It's always normal time.\r
1335 +     *\r
1336 +     * REVISIT: This is probably not a solution that universally\r
1337 +     * works. Just make sure DST is not taken into account. We don't\r
1338 +     * want rounding to be affected by DST.\r
1339 +     */\r
1340 +    tm.tm_isdst = -1;\r
1341 +\r
1342 +    /* special case: rounding with week accuracy */\r
1343 +    if (week_round != PARSE_TIME_NO_ROUND) {\r
1344 +       /* normalize to get proper tm.wday */\r
1345 +       r = normalize_tm (&tm);\r
1346 +       if (r < 0)\r
1347 +           return r;\r
1348 +\r
1349 +       /* set more accurate fields back to zero */\r
1350 +       tm.tm_sec = 0;\r
1351 +       tm.tm_min = 0;\r
1352 +       tm.tm_hour = 0;\r
1353 +       tm.tm_isdst = -1;\r
1354 +\r
1355 +       /* monday is the true 1st day of week, but this is easier */\r
1356 +       if (week_round <= PARSE_TIME_ROUND_DOWN)\r
1357 +           tm.tm_mday -= tm.tm_wday;\r
1358 +       else\r
1359 +           tm.tm_mday += 7 - tm.tm_wday;\r
1360 +    }\r
1361 +\r
1362 +    /* if TZ specified, convert from TZ to local time for mktime(3) */\r
1363 +    if (is_field_set (state, TM_TZ)) {\r
1364 +       time_t t = mktime (&tm);\r
1365 +\r
1366 +       /* from specified TZ to UTC */\r
1367 +       tm.tm_min -= get_field (state, TM_TZ);\r
1368 +\r
1369 +       /* from UTC to local TZ (yes, it's hacky - FIXME) */\r
1370 +       tm.tm_sec += difftime (mktime (localtime (&t)), mktime (gmtime (&t)));\r
1371 +    }\r
1372 +\r
1373 +    /* FIXME: check return value, don't set if fail */\r
1374 +    *t_out = mktime (&tm);\r
1375 +\r
1376 +    return 0;\r
1377 +}\r
1378 +\r
1379 +/* internally, all errors are < 0. parse_time_string() returns errors > 0. */\r
1380 +#define EXTERNAL_ERR(r) (-r)\r
1381 +\r
1382 +int\r
1383 +parse_time_string (const char *s, time_t *t, const time_t *now, int round)\r
1384 +{\r
1385 +    struct state state = { { 0 } };\r
1386 +    int r;\r
1387 +\r
1388 +    if (!s || !t)\r
1389 +       return EXTERNAL_ERR (-PARSE_TIME_ERR);\r
1390 +\r
1391 +    r = parse_input (&state, s);\r
1392 +    if (r < 0)\r
1393 +       return EXTERNAL_ERR (r);\r
1394 +\r
1395 +    r = create_output (&state, t, now, round);\r
1396 +    if (r < 0)\r
1397 +       return EXTERNAL_ERR (r);\r
1398 +\r
1399 +    return 0;\r
1400 +}\r
1401 diff --git a/lib/parse-time-string.h b/lib/parse-time-string.h\r
1402 new file mode 100644\r
1403 index 0000000..50b7c6f\r
1404 --- /dev/null\r
1405 +++ b/lib/parse-time-string.h\r
1406 @@ -0,0 +1,95 @@\r
1407 +/*\r
1408 + * parse time string - user friendly date and time parser\r
1409 + * Copyright © 2012 Jani Nikula\r
1410 + *\r
1411 + * This program is free software: you can redistribute it and/or modify\r
1412 + * it under the terms of the GNU General Public License as published by\r
1413 + * the Free Software Foundation, either version 2 of the License, or\r
1414 + * (at your option) any later version.\r
1415 + *\r
1416 + * This program is distributed in the hope that it will be useful,\r
1417 + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
1418 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
1419 + * GNU General Public License for more details.\r
1420 + *\r
1421 + * You should have received a copy of the GNU General Public License\r
1422 + * along with this program.  If not, see <http://www.gnu.org/licenses/>.\r
1423 + *\r
1424 + * Author: Jani Nikula <jani@nikula.org>\r
1425 + */\r
1426 +\r
1427 +#ifndef PARSE_TIME_STRING_H\r
1428 +#define PARSE_TIME_STRING_H\r
1429 +\r
1430 +#ifdef __cplusplus\r
1431 +extern "C" {\r
1432 +#endif\r
1433 +\r
1434 +#include <time.h>\r
1435 +\r
1436 +/* return values for parse_time_string() */\r
1437 +enum {\r
1438 +    PARSE_TIME_OK = 0,\r
1439 +    PARSE_TIME_ERR,            /* unspecified error */\r
1440 +    PARSE_TIME_ERR_LIB,                /* library call failed */\r
1441 +    PARSE_TIME_ERR_ALREADYSET, /* attempt to set unit twice */\r
1442 +    PARSE_TIME_ERR_FORMAT,     /* generic date/time format error */\r
1443 +    PARSE_TIME_ERR_DATEFORMAT, /* date format error */\r
1444 +    PARSE_TIME_ERR_TIMEFORMAT, /* time format error */\r
1445 +    PARSE_TIME_ERR_INVALIDDATE,        /* date value error */\r
1446 +    PARSE_TIME_ERR_INVALIDTIME,        /* time value error */\r
1447 +    PARSE_TIME_ERR_KEYWORD,    /* unknown keyword */\r
1448 +};\r
1449 +\r
1450 +/* round values for parse_time_string() */\r
1451 +enum {\r
1452 +    PARSE_TIME_ROUND_DOWN = -1,\r
1453 +    PARSE_TIME_NO_ROUND = 0,\r
1454 +    PARSE_TIME_ROUND_UP = 1,\r
1455 +};\r
1456 +\r
1457 +/**\r
1458 + * parse_time_string() - user friendly date and time parser\r
1459 + * @s:         string to parse\r
1460 + * @t:         pointer to time_t to store parsed time in\r
1461 + * @now:       pointer to time_t containing reference date/time, or NULL\r
1462 + * @round:     PARSE_TIME_NO_ROUND, PARSE_TIME_ROUND_DOWN, or\r
1463 + *             PARSE_TIME_ROUND_UP\r
1464 + *\r
1465 + * Parse a date/time string 's' and store the parsed date/time result\r
1466 + * in 't'.\r
1467 + *\r
1468 + * A reference date/time is used for determining the "date/time units"\r
1469 + * (roughly equivalent to struct tm members) not specified by 's'. If\r
1470 + * 'now' is non-NULL, it must contain a pointer to a time_t to be used\r
1471 + * as reference date/time. Otherwise, the current time is used.\r
1472 + *\r
1473 + * If 's' does not specify a full date/time, the 'round' parameter\r
1474 + * specifies if and how the result should be rounded as follows:\r
1475 + *\r
1476 + *   PARSE_TIME_NO_ROUND: All date/time units that are not specified\r
1477 + *   by 's' are set to the corresponding unit derived from the\r
1478 + *   reference date/time.\r
1479 + *\r
1480 + *   PARSE_TIME_ROUND_DOWN: All date/time units that are more accurate\r
1481 + *   than the most accurate unit specified by 's' are set to the\r
1482 + *   smallest valid value for that unit. Rest of the unspecified units\r
1483 + *   are set as in PARSE_TIME_NO_ROUND.\r
1484 + *\r
1485 + *   PARSE_TIME_ROUND_UP: All date/time units that are more accurate\r
1486 + *   than the most accurate unit specified by 's' are set to the\r
1487 + *   smallest valid value for that unit. The most accurate unit\r
1488 + *   specified by 's' is incremented by one (and this is rolled over\r
1489 + *   to the less accurate units as necessary). Rest of the unspecified\r
1490 + *   units are set as in PARSE_TIME_NO_ROUND.\r
1491 + *\r
1492 + * Return 0 (PARSE_TIME_OK) for succesfully parsed date/time, or one\r
1493 + * of PARSE_TIME_ERR_* on error. 't' is not modified on error.\r
1494 + */\r
1495 +int parse_time_string (const char *s, time_t *t, const time_t *now, int round);\r
1496 +\r
1497 +#ifdef __cplusplus\r
1498 +}\r
1499 +#endif\r
1500 +\r
1501 +#endif /* PARSE_TIME_STRING_H */\r
1502 -- \r
1503 1.7.5.4\r
1504 \r