Re: [RFC PATCH 1/2] lib: add date/time parser
authorJani Nikula <jani@nikula.org>
Sun, 26 Feb 2012 20:39:47 +0000 (22:39 +0200)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:45:00 +0000 (09:45 -0800)
73/c5d396fda6c00277bf3adcd097384e19a0a63b [new file with mode: 0644]

diff --git a/73/c5d396fda6c00277bf3adcd097384e19a0a63b b/73/c5d396fda6c00277bf3adcd097384e19a0a63b
new file mode 100644 (file)
index 0000000..8cd6735
--- /dev/null
@@ -0,0 +1,1627 @@
+Return-Path: <jani@nikula.org>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id 7EAAC431FBD\r
+       for <notmuch@notmuchmail.org>; Sun, 26 Feb 2012 12:39:59 -0800 (PST)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -0.7\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5\r
+       tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id AB3oNUuqA-Ah for <notmuch@notmuchmail.org>;\r
+       Sun, 26 Feb 2012 12:39:54 -0800 (PST)\r
+Received: from mail-lpp01m010-f53.google.com (mail-lpp01m010-f53.google.com\r
+       [209.85.215.53]) (using TLSv1 with cipher RC4-SHA (128/128 bits))\r
+       (No client certificate requested)\r
+       by olra.theworths.org (Postfix) with ESMTPS id 10F3E431FBC\r
+       for <notmuch@notmuchmail.org>; Sun, 26 Feb 2012 12:39:53 -0800 (PST)\r
+Received: by lahd3 with SMTP id d3so4805941lah.26\r
+       for <notmuch@notmuchmail.org>; Sun, 26 Feb 2012 12:39:52 -0800 (PST)\r
+Received-SPF: pass (google.com: domain of jani@nikula.org designates\r
+       10.112.44.225 as permitted sender) client-ip=10.112.44.225; \r
+Authentication-Results: mr.google.com;\r
+       spf=pass (google.com: domain of jani@nikula.org\r
+       designates 10.112.44.225 as permitted sender)\r
+       smtp.mail=jani@nikula.org\r
+Received: from mr.google.com ([10.112.44.225])\r
+       by 10.112.44.225 with SMTP id h1mr1033470lbm.34.1330288792469 (num_hops\r
+       = 1); Sun, 26 Feb 2012 12:39:52 -0800 (PST)\r
+Received: by 10.112.44.225 with SMTP id h1mr893089lbm.34.1330288792308;\r
+       Sun, 26 Feb 2012 12:39:52 -0800 (PST)\r
+Received: from localhost (dsl-hkibrasgw4-fe50f800-253.dhcp.inet.fi.\r
+       [84.248.80.253])\r
+       by mx.google.com with ESMTPS id v7sm11521693lad.5.2012.02.26.12.39.48\r
+       (version=SSLv3 cipher=OTHER); Sun, 26 Feb 2012 12:39:50 -0800 (PST)\r
+From: Jani Nikula <jani@nikula.org>\r
+To: Mark Walters <markwalters1009@gmail.com>, notmuch@notmuchmail.org\r
+Subject: Re: [RFC PATCH 1/2] lib: add date/time parser\r
+In-Reply-To: <87mx86dlul.fsf@qmul.ac.uk>\r
+References: <cover.1329689945.git.jani@nikula.org>\r
+       <25ea278088dd81c51496825c4f8365796de29094.1329689945.git.jani@nikula.org>\r
+       <87mx86dlul.fsf@qmul.ac.uk>\r
+User-Agent: Notmuch/0.11.1+206~gc02a53f (http://notmuchmail.org) Emacs/23.3.1\r
+       (i686-pc-linux-gnu)\r
+Date: Sun, 26 Feb 2012 22:39:47 +0200\r
+Message-ID: <87399x5nxo.fsf@nikula.org>\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=utf-8\r
+Content-Transfer-Encoding: quoted-printable\r
+X-Gm-Message-State:\r
+ ALoCoQm+ecxXLbCqBzDruD9rIscKkZ7wzZEgLycMJ9MO9YFLCqxKuNTtS3hCa3RtQSdSIY9oHqg2\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Sun, 26 Feb 2012 20:39:59 -0000\r
+\r
+On Sun, 26 Feb 2012 08:45:22 +0000, Mark Walters <markwalters1009@gmail.com=\r
+> wrote:\r
+>=20\r
+> Hi I have not read all of this carefully but it looks very nice to\r
+> me. It is pleasantly nice to read.=20\r
+\r
+Thank you!\r
+\r
+> I have not looked through the create output function yet but have looked\r
+> at most of the rest.\r
+\r
+There are a few rough edges in the output part still, but as you've\r
+perhaps noticed it's a completely separate stage from the parsing bit.\r
+\r
+> My only concern (as mentioned on irc) is the question of\r
+> internationalisation. I think most of the this can be done by allowing\r
+> other keyword tables and that seems quite clean. Ideally I think the\r
+> user would set which to localisation use in the config file and then the\r
+> cli would pass that to the lib parser.\r
+\r
+I think it might be possible to tweak the table and the keyword matching\r
+in a way that makes the ordering in the table unimportant. The strings\r
+themselves could contain information about the abbreviation points and\r
+priority wrt same length matches, and those could be part of the\r
+translation through gettext. And I don't think it would even bloat the\r
+code much.\r
+\r
+Assuming gettext would be the internationalization method of choice.\r
+\r
+> I think it would be a shame to hold up this very useful functionality\r
+> just because of these internationalisation concerns.\r
+\r
+I agree but I'm heavily biased! ;)\r
+\r
+> The code is fairly large but it is easy to read and I would imagine\r
+> (excepting the internationalisation question) almost maintenance free.\r
+>=20\r
+> On the actual code I have a small number of comments/queries below.\r
+\r
+Replies to them inline, and fixes pushed to [1] along with some other\r
+fixes and improvements.\r
+\r
+BR,\r
+Jani.\r
+\r
+\r
+[1] http://gitorious.org/parse-time-string\r
+\r
+>=20\r
+> Best wishes\r
+>=20\r
+> Mark\r
+>=20\r
+> On Mon, 20 Feb 2012 00:55:51 +0200, Jani Nikula <jani@nikula.org> wrote:\r
+> > Signed-off-by: Jani Nikula <jani@nikula.org>\r
+> > ---\r
+> >  lib/Makefile.local      |    1 +\r
+> >  lib/parse-time-string.c | 1304 +++++++++++++++++++++++++++++++++++++++=\r
+++++++++\r
+> >  lib/parse-time-string.h |   95 ++++\r
+> >  3 files changed, 1400 insertions(+), 0 deletions(-)\r
+> >  create mode 100644 lib/parse-time-string.c\r
+> >  create mode 100644 lib/parse-time-string.h\r
+> >=20\r
+> > diff --git a/lib/Makefile.local b/lib/Makefile.local\r
+> > index 54c4dea..803a284 100644\r
+> > --- a/lib/Makefile.local\r
+> > +++ b/lib/Makefile.local\r
+> > @@ -53,6 +53,7 @@ libnotmuch_c_srcs =3D            \\r
+> >    $(dir)/libsha1.c        \\r
+> >    $(dir)/message-file.c   \\r
+> >    $(dir)/messages.c       \\r
+> > +  $(dir)/parse-time-string.c      \\r
+> >    $(dir)/sha1.c           \\r
+> >    $(dir)/tags.c\r
+> >=20=20\r
+> > diff --git a/lib/parse-time-string.c b/lib/parse-time-string.c\r
+> > new file mode 100644\r
+> > index 0000000..59713dc\r
+> > --- /dev/null\r
+> > +++ b/lib/parse-time-string.c\r
+> > @@ -0,0 +1,1304 @@\r
+> > +/*\r
+> > + * parse time string - user friendly date and time parser\r
+> > + * Copyright =C2=A9 2012 Jani Nikula\r
+> > + *\r
+> > + * This program is free software: you can redistribute it and/or modify\r
+> > + * it under the terms of the GNU General Public License as published by\r
+> > + * the Free Software Foundation, either version 2 of the License, or\r
+> > + * (at your option) any later version.\r
+> > + *\r
+> > + * This program is distributed in the hope that it will be useful,\r
+> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+> > + * GNU General Public License for more details.\r
+> > + *\r
+> > + * You should have received a copy of the GNU General Public License\r
+> > + * along with this program.  If not, see <http://www.gnu.org/licenses/=\r
+>.\r
+> > + *\r
+> > + * Author: Jani Nikula <jani@nikula.org>\r
+> > + */\r
+> > +\r
+> > +#ifndef PARSE_TIME_DEBUG\r
+> > +#define NDEBUG /* for assert() */\r
+> > +#endif\r
+> > +\r
+> > +#include <assert.h>\r
+> > +#include <ctype.h>\r
+> > +#include <errno.h>\r
+> > +#include <limits.h>\r
+> > +#include <stdio.h>\r
+> > +#include <stdarg.h>\r
+> > +#include <stdbool.h>\r
+> > +#include <stdlib.h>\r
+> > +#include <string.h>\r
+> > +#include <strings.h>\r
+> > +#include <time.h>\r
+> > +#include <sys/time.h>\r
+> > +#include <sys/types.h>\r
+> > +\r
+> > +#include "parse-time-string.h"\r
+> > +\r
+> > +#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))\r
+> > +\r
+> > +/* field indices in struct state tm, and set fields */\r
+> > +enum field {\r
+> > +    /* keep SEC...YEAR in this order */\r
+> > +    TM_ABS_SEC,           /* seconds */\r
+> > +    TM_ABS_MIN,           /* minutes */\r
+> > +    TM_ABS_HOUR,  /* hours */\r
+> > +    TM_ABS_MDAY,  /* day of the month */\r
+> > +    TM_ABS_MON,           /* month */\r
+> > +    TM_ABS_YEAR,  /* year */\r
+> > +\r
+> > +    TM_ABS_WDAY,  /* day of the week. special: may be relative */\r
+> > +    TM_ABS_ISDST, /* daylight saving time */\r
+> > +\r
+> > +    TM_AMPM,              /* am vs. pm */\r
+> > +    TM_TZ,                /* timezone in minutes */\r
+> > +\r
+> > +    /* keep SEC...YEAR in this order */\r
+> > +    TM_REL_SEC,           /* seconds relative to now */\r
+> > +    TM_REL_MIN,           /* minutes ... */\r
+> > +    TM_REL_HOUR,  /* hours ... */\r
+> > +    TM_REL_DAY,           /* days ... */\r
+> > +    TM_REL_MON,           /* months ... */\r
+> > +    TM_REL_YEAR,  /* years ... */\r
+> > +    TM_REL_WEEK,  /* weeks ... */\r
+> > +\r
+> > +    TM_NONE,              /* not a field */\r
+> > +\r
+> > +    TM_SIZE =3D TM_NONE,\r
+> > +};\r
+> > +\r
+> > +enum field_set {\r
+> > +    FIELD_UNSET,\r
+> > +    FIELD_SET,\r
+> > +    FIELD_NOW,\r
+> > +};\r
+> > +\r
+> > +static enum field\r
+> > +next_field (enum field field)\r
+> > +{\r
+> > +    /* note: depends on the enum ordering */\r
+> > +    return field < TM_ABS_YEAR ? field + 1 : TM_NONE;\r
+> > +}\r
+> > +\r
+> > +static enum field\r
+> > +abs_to_rel_field (enum field field)\r
+> > +{\r
+> > +    assert (field <=3D TM_ABS_YEAR);\r
+> > +\r
+> > +    /* note: depends on the enum ordering */\r
+> > +    return field + (TM_REL_SEC - TM_ABS_SEC);\r
+> > +}\r
+> > +\r
+> > +/* get zero value for field */\r
+> > +static int\r
+> > +field_zero (enum field field)\r
+> > +{\r
+> > +    if (field =3D=3D TM_ABS_MDAY || field =3D=3D TM_ABS_MON)\r
+> > +  return 1;\r
+> > +    else if (field =3D=3D TM_ABS_YEAR)\r
+> > +  return 1970;\r
+> > +    else\r
+> > +  return 0;\r
+> > +}\r
+> > +\r
+> > +struct state {\r
+> > +    int tm[TM_SIZE];                      /* parsed date and time */\r
+> > +    enum field_set set[TM_SIZE];  /* set status of tm */\r
+> > +\r
+> > +    enum field last_field;\r
+> > +    char delim;\r
+> > +\r
+> > +    int postponed_length; /* number of digits in postponed value */\r
+> > +    int postponed_value;\r
+> > +};\r
+>=20\r
+> Personally I would prefer this above the function definitions (but\r
+> obviously that is up to you).\r
+\r
+I just wanted to keep the couple of enum field related functions near\r
+the enum field definition because they have a dependency on the enum\r
+ordering.\r
+\r
+>=20\r
+> > +\r
+> > +/*\r
+> > + * Helpers for postponed numbers.\r
+> > + *\r
+> > + * postponed_length is the number of digits in postponed value. 0\r
+> > + * means there is no postponed number. -1 means there is a postponed\r
+> > + * number, but it comes from a keyword, and it doesn't have digits.\r
+> > + */\r
+> > +static int\r
+> > +get_postponed_length (struct state *state)\r
+> > +{\r
+> > +    return state->postponed_length;\r
+> > +}\r
+> > +\r
+> > +static bool\r
+> > +get_postponed_number (struct state *state, int *v, int *n)\r
+> > +{\r
+> > +    if (!state->postponed_length)\r
+> > +  return false;\r
+> > +\r
+> > +    if (n)\r
+> > +  *n =3D state->postponed_length;\r
+> > +\r
+> > +    if (v)\r
+> > +  *v =3D state->postponed_value;\r
+> > +\r
+> > +    state->postponed_length =3D 0;\r
+> > +    state->postponed_value =3D 0;\r
+> > +\r
+> > +    return true;\r
+> > +}\r
+> > +\r
+> > +/* parse postponed number if one exists */\r
+> > +static int parse_postponed_number (struct state *state, int v, int n);\r
+> > +static int\r
+> > +handle_postponed_number (struct state *state)\r
+> > +{\r
+> > +    int v =3D state->postponed_value;\r
+> > +    int n =3D state->postponed_length;\r
+> > +\r
+> > +    if (!n)\r
+> > +  return 0;\r
+> > +\r
+> > +    state->postponed_value =3D 0;\r
+> > +    state->postponed_length =3D 0;\r
+> > +\r
+> > +    return parse_postponed_number (state, v, n);\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * set new postponed number to be handled later. if one exists\r
+> > + * already, handle it first. n may be -1 to indicate a keyword that\r
+> > + * has no number length.\r
+> > + */\r
+> > +static int\r
+> > +set_postponed_number (struct state *state, int v, int n)\r
+> > +{\r
+> > +    int r;\r
+> > +\r
+> > +    /* parse previous postponed number, if any */\r
+> > +    r =3D handle_postponed_number (state);\r
+> > +    if (r)\r
+> > +  return r;\r
+> > +\r
+> > +    state->postponed_length =3D n;\r
+> > +    state->postponed_value =3D v;\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +static void\r
+> > +set_delim (struct state *state, char delim)\r
+> > +{\r
+> > +    state->delim =3D delim;\r
+> > +}\r
+> > +\r
+> > +static void\r
+> > +unset_delim (struct state *state)\r
+> > +{\r
+> > +    state->delim =3D 0;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Field set/get/mod helpers.\r
+> > + */\r
+> > +\r
+> > +/* returns unset for non-tracked fields */\r
+> > +static bool\r
+> > +is_field_set (struct state *state, enum field field)\r
+> > +{\r
+> > +    assert (field < ARRAY_SIZE (state->tm));\r
+> > +\r
+> > +    return field < ARRAY_SIZE (state->set) &&\r
+> > +     state->set[field] !=3D FIELD_UNSET;\r
+> > +}\r
+> > +\r
+> > +static void\r
+> > +unset_field (struct state *state, enum field field)\r
+> > +{\r
+> > +    assert (field < ARRAY_SIZE (state->tm));\r
+> > +\r
+> > +    state->set[field] =3D FIELD_UNSET;\r
+> > +    state->tm[field] =3D 0;\r
+> > +}\r
+> > +\r
+> > +/* Set field to value. */\r
+> > +static int\r
+> > +set_field (struct state *state, enum field field, int value)\r
+> > +{\r
+> > +    int r;\r
+> > +\r
+> > +    assert (field < ARRAY_SIZE (state->tm));\r
+> > +\r
+> > +    /* some fields can only be set once */\r
+> > +    if (field < ARRAY_SIZE (state->set) && state->set[field] !=3D FIEL=\r
+D_UNSET)\r
+> > +  return -PARSE_TIME_ERR_ALREADYSET;\r
+> > +\r
+> > +    state->set[field] =3D FIELD_SET;\r
+> > +\r
+> > +    /*\r
+> > +     * REVISIT: There could be a "next_field" that would be set from\r
+> > +     * "field" for the duration of the handle_postponed_number() call,\r
+> > +     * so it has more information to work with.\r
+> > +     */\r
+> > +\r
+> > +    /* parse postponed number, if any */\r
+> > +    r =3D handle_postponed_number (state);\r
+> > +    if (r)\r
+> > +  return r;\r
+> > +\r
+> > +    unset_delim (state);\r
+> > +\r
+> > +    state->tm[field] =3D value;\r
+> > +    state->last_field =3D field;\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Mark n fields in fields to be set to current date/time in the\r
+> > + * specified time zone, or local timezone if not specified. The fields\r
+> > + * will be initialized after parsing is complete and timezone is\r
+> > + * known.\r
+> > + */\r
+> > +static int\r
+> > +set_fields_to_now (struct state *state, enum field *fields, size_t n)\r
+> > +{\r
+> > +    size_t i;\r
+> > +    int r;\r
+> > +\r
+> > +    for (i =3D 0; i < n; i++) {\r
+> > +  r =3D set_field (state, fields[i], 0);\r
+> > +  if (r)\r
+> > +      return r;\r
+> > +  state->set[fields[i]] =3D FIELD_NOW;\r
+> > +    }\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/* Modify field by adding value to it. To be used on relative fields. =\r
+*/\r
+> > +static int\r
+> > +mod_field (struct state *state, enum field field, int value)\r
+> > +{\r
+> > +    int r;\r
+> > +\r
+> > +    assert (field < ARRAY_SIZE (state->tm));   /* assert relative??? */\r
+> > +\r
+> > +    if (field < ARRAY_SIZE (state->set))\r
+> > +  state->set[field] =3D FIELD_SET;\r
+> > +\r
+> > +    /* parse postponed number, if any */\r
+> > +    r =3D handle_postponed_number (state);\r
+> > +    if (r)\r
+> > +  return r;\r
+> > +\r
+> > +    unset_delim (state);\r
+> > +\r
+> > +    state->tm[field] +=3D value;\r
+> > +    state->last_field =3D field;\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Get field value. Make sure the field is set before query. It's most\r
+> > + * likely an error to call this while parsing (for example fields set\r
+> > + * as FIELD_NOW will only be set to some value after parsing).\r
+> > + */\r
+> > +static int\r
+> > +get_field (struct state *state, enum field field)\r
+> > +{\r
+> > +    assert (field < ARRAY_SIZE (state->tm));\r
+> > +\r
+> > +    return state->tm[field];\r
+> > +}\r
+> > +\r
+> > +/* Unset indicator for time and date set helpers. */\r
+> > +#define UNSET -1\r
+> > +\r
+> > +/* Time set helper. No input checking. Use UNSET (-1) to leave unset. =\r
+*/\r
+> > +static int\r
+> > +set_abs_time (struct state *state, int hour, int min, int sec)\r
+> > +{\r
+> > +    int r;\r
+> > +\r
+> > +    if (hour !=3D UNSET) {\r
+> > +  if ((r =3D set_field (state, TM_ABS_HOUR, hour)))\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    if (min !=3D UNSET) {\r
+> > +  if ((r =3D set_field (state, TM_ABS_MIN, min)))\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    if (sec !=3D UNSET) {\r
+> > +  if ((r =3D set_field (state, TM_ABS_SEC, sec)))\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/* Date set helper. No input checking. Use UNSET (-1) to leave unset. =\r
+*/\r
+> > +static int\r
+> > +set_abs_date (struct state *state, int year, int mon, int mday)\r
+> > +{\r
+> > +    int r;\r
+> > +\r
+> > +    if (year !=3D UNSET) {\r
+> > +  if ((r =3D set_field (state, TM_ABS_YEAR, year)))\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    if (mon !=3D UNSET) {\r
+> > +  if ((r =3D set_field (state, TM_ABS_MON, mon)))\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    if (mday !=3D UNSET) {\r
+> > +  if ((r =3D set_field (state, TM_ABS_MDAY, mday)))\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Keyword parsing and handling.\r
+> > + */\r
+> > +struct keyword;\r
+> > +typedef int (*setter_t)(struct state *state, struct keyword *kw);\r
+> > +\r
+> > +struct keyword {\r
+> > +    const char *name;     /* keyword */\r
+> > +    size_t minlen;        /* min length to match, 0 =3D must match all */\r
+> > +    enum field field;     /* field to set, or FIELD_NONE if N/A */\r
+> > +    int value;            /* value to set, or 0 if N/A */\r
+> > +    setter_t set; /* function to use for setting, if non-NULL */\r
+> > +};\r
+> > +\r
+> > +/*\r
+> > + * Setter callback functions for keywords.\r
+> > + */\r
+> > +static int\r
+> > +kw_set_default (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    return set_field (state, kw->field, kw->value);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_rel (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    int multiplier =3D 1;\r
+> > +\r
+> > +    /* get a previously set multiplier, if any */\r
+> > +    get_postponed_number (state, &multiplier, NULL);\r
+> > +\r
+> > +    /* accumulate relative field values */\r
+> > +    return mod_field (state, kw->field, multiplier * kw->value);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_number (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    /* -1 =3D no length, from keyword */\r
+> > +    return set_postponed_number (state, kw->value, -1);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_month (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    int n =3D get_postponed_length (state);\r
+> > +\r
+> > +    /* consume postponed number if it could be mday */\r
+> > +    if (n =3D=3D 1 || n =3D=3D 2) {\r
+> > +  int r, v;\r
+> > +\r
+> > +  get_postponed_number (state, &v, NULL);\r
+> > +\r
+> > +  if (v < 1 || v > 31)\r
+> > +      return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +\r
+> > +  r =3D set_field (state, TM_ABS_MDAY, v);\r
+> > +  if (r)\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    return set_field (state, kw->field, kw->value);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_ampm (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    int n =3D get_postponed_length (state);\r
+> > +\r
+> > +    /* consume postponed number if it could be hour */\r
+> > +    if (n =3D=3D 1 || n =3D=3D 2) {\r
+> > +  int r, v;\r
+> > +\r
+> > +  get_postponed_number (state, &v, NULL);\r
+> > +\r
+> > +  if (v < 1 || v > 12)\r
+> > +      return -PARSE_TIME_ERR_INVALIDTIME;\r
+> > +\r
+> > +  r =3D set_abs_time (state, v, 0, 0);\r
+> > +  if (r)\r
+> > +      return r;\r
+> > +    }\r
+> > +\r
+> > +    return set_field (state, kw->field, kw->value);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_timeofday (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    return set_abs_time (state, kw->value, 0, 0);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_today (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    enum field fields[] =3D { TM_ABS_YEAR, TM_ABS_MON, TM_ABS_MDAY };\r
+> > +\r
+> > +    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_now (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    enum field fields[] =3D { TM_ABS_HOUR, TM_ABS_MIN, TM_ABS_SEC };\r
+> > +\r
+> > +    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +kw_set_ordinal (struct state *state, struct keyword *kw)\r
+> > +{\r
+> > +    int n, v;\r
+> > +\r
+> > +    /* require a postponed number */\r
+> > +    if (!get_postponed_number (state, &v, &n))\r
+> > +  return -PARSE_TIME_ERR_DATEFORMAT;\r
+> > +\r
+> > +    /* ordinals are mday */\r
+> > +    if (n !=3D 1 && n !=3D 2)\r
+> > +  return -PARSE_TIME_ERR_DATEFORMAT;\r
+> > +\r
+> > +    /* be strict about st, nd, rd, and lax about th */\r
+> > +    if (strcasecmp (kw->name, "st") =3D=3D 0 && v !=3D 1 && v !=3D 21 =\r
+&& v !=3D 31)\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +    else if (strcasecmp (kw->name, "nd") =3D=3D 0 && v !=3D 2 && v !=\r
+=3D 22)\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +    else if (strcasecmp (kw->name, "rd") =3D=3D 0 && v !=3D 3 && v !=\r
+=3D 23)\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +    else if (strcasecmp (kw->name, "th") =3D=3D 0 && (v < 1 || v > 31))\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +\r
+> > +    return set_field (state, TM_ABS_MDAY, v);\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Accepted keywords.\r
+> > + *\r
+> > + * If keyword begins with upper case letter, then the matching will be\r
+> > + * case sensitive. Otherwise the matching is case insensitive.\r
+> > + *\r
+> > + * If setter is NULL, set_default will be used.\r
+> > + *\r
+> > + * Note: Order matters. Matching is greedy, longest match is used, but\r
+> > + * of equal length matches the first one is used.\r
+> > + */\r
+> > +static struct keyword keywords[] =3D {\r
+> > +    /* weekdays */\r
+> > +    { "sunday",           3,      TM_ABS_WDAY,    0,      NULL },\r
+> > +    { "monday",           3,      TM_ABS_WDAY,    1,      NULL },\r
+> > +    { "tuesday",  3,      TM_ABS_WDAY,    2,      NULL },\r
+> > +    { "wednesday",        3,      TM_ABS_WDAY,    3,      NULL },\r
+> > +    { "thursday", 3,      TM_ABS_WDAY,    4,      NULL },\r
+> > +    { "friday",           3,      TM_ABS_WDAY,    5,      NULL },\r
+> > +    { "saturday", 3,      TM_ABS_WDAY,    6,      NULL },\r
+> > +\r
+> > +    /* months */\r
+> > +    { "january",  3,      TM_ABS_MON,     1,      kw_set_month },\r
+> > +    { "february", 3,      TM_ABS_MON,     2,      kw_set_month },\r
+> > +    { "march",            3,      TM_ABS_MON,     3,      kw_set_month },\r
+> > +    { "april",            3,      TM_ABS_MON,     4,      kw_set_month },\r
+> > +    { "may",              3,      TM_ABS_MON,     5,      kw_set_month },\r
+> > +    { "june",             3,      TM_ABS_MON,     6,      kw_set_month },\r
+> > +    { "july",             3,      TM_ABS_MON,     7,      kw_set_month },\r
+> > +    { "august",           3,      TM_ABS_MON,     8,      kw_set_month },\r
+> > +    { "september",        3,      TM_ABS_MON,     9,      kw_set_month },\r
+> > +    { "october",  3,      TM_ABS_MON,     10,     kw_set_month },\r
+> > +    { "november", 3,      TM_ABS_MON,     11,     kw_set_month },\r
+> > +    { "december", 3,      TM_ABS_MON,     12,     kw_set_month },\r
+> > +\r
+> > +    /* durations */\r
+> > +    { "years",            1,      TM_REL_YEAR,    1,      kw_set_rel },\r
+> > +    { "weeks",            1,      TM_REL_WEEK,    1,      kw_set_rel },\r
+> > +    { "days",             1,      TM_REL_DAY,     1,      kw_set_rel },\r
+> > +    { "hours",            1,      TM_REL_HOUR,    1,      kw_set_rel },\r
+> > +    { "hrs",              1,      TM_REL_HOUR,    1,      kw_set_rel },\r
+> > +    /* M=3Dmonths, m=3Dminutes. single M must precede minutes in the l=\r
+ist. */\r
+> > +    { "M",                1,      TM_REL_MON,     1,      kw_set_rel },\r
+> > +    { "minutes",  1,      TM_REL_MIN,     1,      kw_set_rel },\r
+> > +    { "mins",             1,      TM_REL_MIN,     1,      kw_set_rel },\r
+> > +    { "months",           1,      TM_REL_MON,     1,      kw_set_rel },\r
+> > +    { "seconds",  1,      TM_REL_SEC,     1,      kw_set_rel },\r
+> > +    { "secs",             1,      TM_REL_SEC,     1,      kw_set_rel },\r
+> > +\r
+> > +    /* numbers */\r
+> > +    { "one",              0,      TM_NONE,        1,      kw_set_number },\r
+> > +    { "two",              0,      TM_NONE,        2,      kw_set_number },\r
+> > +    { "three",            0,      TM_NONE,        3,      kw_set_number },\r
+> > +    { "four",             0,      TM_NONE,        4,      kw_set_number },\r
+> > +    { "five",             0,      TM_NONE,        5,      kw_set_number },\r
+> > +    { "six",              0,      TM_NONE,        6,      kw_set_number },\r
+> > +    { "seven",            0,      TM_NONE,        7,      kw_set_number },\r
+> > +    { "eight",            0,      TM_NONE,        8,      kw_set_number },\r
+> > +    { "nine",             0,      TM_NONE,        9,      kw_set_number },\r
+> > +    { "ten",              0,      TM_NONE,        10,     kw_set_number },\r
+> > +    { "dozen",            0,      TM_NONE,        12,     kw_set_number },\r
+> > +    { "hundred",  0,      TM_NONE,        100,    kw_set_number },\r
+> > +\r
+> > +    /* special number forms */\r
+> > +    { "this",             0,      TM_NONE,        0,      kw_set_number },\r
+> > +    { "last",             0,      TM_NONE,        1,      kw_set_number },\r
+> > +\r
+> > +    /* specials */\r
+> > +    { "yesterday",        0,      TM_REL_DAY,     1,      kw_set_rel },\r
+> > +    { "today",            0,      TM_NONE,        0,      kw_set_today },\r
+> > +    { "now",              0,      TM_NONE,        0,      kw_set_now },\r
+> > +    { "noon",             0,      TM_NONE,        12,     kw_set_timeofday },\r
+> > +    { "midnight", 0,      TM_NONE,        0,      kw_set_timeofday },\r
+> > +    { "am",               0,      TM_AMPM,        0,      kw_set_ampm },\r
+> > +    { "a.m.",             0,      TM_AMPM,        0,      kw_set_ampm },\r
+> > +    { "pm",               0,      TM_AMPM,        1,      kw_set_ampm },\r
+> > +    { "p.m.",             0,      TM_AMPM,        1,      kw_set_ampm },\r
+> > +    { "st",               0,      TM_NONE,        0,      kw_set_ordinal },\r
+> > +    { "nd",               0,      TM_NONE,        0,      kw_set_ordinal },\r
+> > +    { "rd",               0,      TM_NONE,        0,      kw_set_ordinal },\r
+> > +    { "th",               0,      TM_NONE,        0,      kw_set_ordinal },\r
+> > +\r
+> > +    /* timezone codes: offset in minutes. FIXME: add more codes. */\r
+> > +    { "pst",              0,      TM_TZ,          -8*60,  NULL },\r
+> > +    { "mst",              0,      TM_TZ,          -7*60,  NULL },\r
+> > +    { "cst",              0,      TM_TZ,          -6*60,  NULL },\r
+> > +    { "est",              0,      TM_TZ,          -5*60,  NULL },\r
+> > +    { "ast",              0,      TM_TZ,          -4*60,  NULL },\r
+> > +    { "nst",              0,      TM_TZ,          -(3*60+30),     NULL },\r
+> > +\r
+> > +    { "gmt",              0,      TM_TZ,          0,      NULL },\r
+> > +    { "utc",              0,      TM_TZ,          0,      NULL },\r
+> > +\r
+> > +    { "wet",              0,      TM_TZ,          0,      NULL },\r
+> > +    { "cet",              0,      TM_TZ,          1*60,   NULL },\r
+> > +    { "eet",              0,      TM_TZ,          2*60,   NULL },\r
+> > +    { "fet",              0,      TM_TZ,          3*60,   NULL },\r
+> > +\r
+> > +    { "wat",              0,      TM_TZ,          1*60,   NULL },\r
+> > +    { "cat",              0,      TM_TZ,          2*60,   NULL },\r
+> > +    { "eat",              0,      TM_TZ,          3*60,   NULL },\r
+> > +};\r
+> > +\r
+> > +/*\r
+> > + * Compare strings s and keyword. Return number of matching chars on\r
+> > + * match, 0 for no match. Match must be at least n chars (n =3D=3D 0 a=\r
+ll\r
+> > + * of keyword), otherwise it's not a match. Use match_case for case\r
+> > + * sensitive matching.\r
+> > + */\r
+> > +static size_t\r
+> > +stringcmp (const char *s, const char *keyword, size_t n, bool match_ca=\r
+se)\r
+> > +{\r
+> > +    size_t i;\r
+> > +\r
+> > +    for (i =3D 0; *s && *keyword; i++, s++, keyword++) {\r
+> > +  if (match_case) {\r
+> > +      if (*s !=3D *keyword)\r
+> > +          break;\r
+> > +  } else {\r
+> > +      if (tolower ((unsigned char) *s) !=3D\r
+> > +          tolower ((unsigned char) *keyword))\r
+> > +          break;\r
+> > +  }\r
+> > +    }\r
+> > +\r
+> > +    if (n)\r
+> > +  return i < n ? 0 : i;\r
+> > +    else\r
+> > +  return *keyword ? 0 : i;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Parse a keyword. Return < 0 on error, number of parsed chars on\r
+> > + * success.\r
+> > + */\r
+> > +static ssize_t\r
+> > +parse_keyword (struct state *state, const char *s)\r
+> > +{\r
+> > +    unsigned int i;\r
+> > +    size_t n, max_n =3D 0;\r
+> > +    struct keyword *kw =3D NULL;\r
+> > +    int r;\r
+> > +\r
+> > +    /* Match longest keyword */\r
+> > +    for (i =3D 0; i < ARRAY_SIZE (keywords); i++) {\r
+> > +  /* Match case if keyword begins with upper case letter. */\r
+> > +  bool mcase =3D isupper ((unsigned char) keywords[i].name[0]);\r
+> > +\r
+> > +  n =3D stringcmp (s, keywords[i].name, keywords[i].minlen, mcase);\r
+> > +  if (n > max_n) {\r
+> > +      max_n =3D n;\r
+> > +      kw =3D &keywords[i];\r
+> > +  }\r
+> > +    }\r
+> > +\r
+> > +    if (!kw)\r
+> > +  return -PARSE_TIME_ERR_KEYWORD;\r
+> > +\r
+> > +    if (kw->set)\r
+> > +  r =3D kw->set (state, kw);\r
+> > +    else\r
+> > +  r =3D kw_set_default (state, kw);\r
+> > +\r
+> > +    return r < 0 ? r : max_n;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Non-keyword parsers and their helpers.\r
+> > + */\r
+> > +\r
+> > +static int\r
+> > +set_user_tz (struct state *state, char sign, int hour, int min)\r
+> > +{\r
+> > +    int tz =3D hour * 60 + min;\r
+> > +\r
+> > +    assert (sign =3D=3D '+' || sign =3D=3D '-');\r
+> > +\r
+> > +    if (hour < 0 || hour > 14 || min < 0 || min > 60 || min % 15)\r
+> > +  return -PARSE_TIME_ERR_INVALIDTIME;\r
+> > +\r
+> > +    if (sign =3D=3D '-')\r
+> > +  tz =3D -tz;\r
+> > +\r
+> > +    return set_field (state, TM_TZ, tz);\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Independent parsing of a postponed number when it wasn't consumed\r
+> > + * during parsing of the following token.\r
+> > + *\r
+> > + * This should be able to trust that last_field and next_field are\r
+> > + * right.\r
+> > + */\r
+> > +static int\r
+> > +parse_postponed_number (struct state *state, int v, int n)\r
+> > +{\r
+> > +    /*\r
+> > +     * alright, these are really lone, won't affect parsing of\r
+> > +     * following items... it's not a multiplier, those have been eaten\r
+> > +     * away.\r
+> > +     *\r
+> > +     * also note numbers eaten away by parse_single_number.\r
+> > +     */\r
+> > +\r
+> > +    assert (n < 8);\r
+> > +\r
+> > +    switch (n) {\r
+> > +    case 1:\r
+> > +    case 2:\r
+> > +  /* hour or mday or year */\r
+> > +  if (state->last_field =3D=3D TM_ABS_MON &&  /* FIXME: written mon! */\r
+> > +      !is_field_set (state, TM_ABS_MDAY)) {\r
+> > +      return set_field (state, TM_ABS_MDAY, v);\r
+> > +  }\r
+> > +  break;\r
+> > +    case 4:\r
+> > +  /* YYYY or +/-HHMM for TZ or HHMM or DDMM */\r
+> > +  /* FIXME: state->delim is no longer right for this function!\r
+> > +   * why not, it could be! */\r
+> > +  if (!is_field_set (state, TM_ABS_YEAR)) {\r
+> > +      /* FIXME: check year? */\r
+> > +      return set_field (state, TM_ABS_YEAR, v);\r
+> > +  }\r
+> > +  break;\r
+> > +    case 6:\r
+> > +  /* FIXME: HHMMSS or DDMMYY */\r
+> > +  break;\r
+> > +    case -1:\r
+> > +  /* REVISIT */\r
+> > +  break;\r
+> > +    case 3:\r
+> > +    case 5:\r
+> > +    case 7:\r
+> > +    default:\r
+> > +  break;\r
+> > +    }\r
+> > +\r
+> > +    return -PARSE_TIME_ERR_FORMAT;\r
+> > +}\r
+> > +\r
+> > +/* Parse a single number. Typically postpone parsing until later. */\r
+> > +static int\r
+> > +parse_single_number (struct state *state, unsigned long v,\r
+> > +               unsigned long n)\r
+> > +{\r
+> > +    assert (n);\r
+> > +\r
+> > +    /* parse things that can be parsed immediately */\r
+> > +    if (n =3D=3D 8) {\r
+> > +  /* YYYYMMDD */\r
+> > +  int year =3D v / 10000;\r
+> > +  int mon =3D (v / 100) % 100;\r
+> > +  int mday =3D v % 100;\r
+> > +\r
+> > +  if (year < 1970 || mon < 1 || mon > 12 || mday < 1 || mday > 31)\r
+> > +      return -PARSE_TIME_ERR_INVALIDDATE;\r
+>=20\r
+> I think dates are checked for validity in more than one place. It might\r
+> be worth pulling that out into a function. In particular, someone might\r
+> want to check mday depending on month at some point.\r
+\r
+Agreed.\r
+\r
+>=20\r
+> > +\r
+> > +  return set_abs_date (state, year, mon, mday);\r
+> > +    } else if (n > 8) {\r
+> > +  /* FIXME: seconds since epoch */\r
+> > +  return -PARSE_TIME_ERR_FORMAT;\r
+> > +    }\r
+>=20\r
+> This is probably an important FIXME for notmuch for backward compatibilit=\r
+y.\r
+\r
+Nice to have, but not hugely important; this does not affect the\r
+existing <timestamp>..<timestamp> (without "date:" prefix!) search.\r
+\r
+>=20\r
+> > +\r
+> > +    if (v > INT_MAX)\r
+> > +  return -PARSE_TIME_ERR_FORMAT;\r
+> > +\r
+> > +    return set_postponed_number (state, v, n);\r
+> > +}\r
+> > +\r
+> > +static bool\r
+> > +is_time_sep (char c)\r
+> > +{\r
+> > +    return c =3D=3D ':';\r
+> > +}\r
+> > +\r
+> > +static bool\r
+> > +is_date_sep (char c)\r
+> > +{\r
+> > +    return c =3D=3D '/' || c =3D=3D '-' || c =3D=3D '.';\r
+> > +}\r
+> > +\r
+> > +static bool\r
+> > +is_sep (char c)\r
+> > +{\r
+> > +    return is_time_sep (c) || is_date_sep (c);\r
+> > +}\r
+> > +\r
+> > +/* two-digit year: 00...69 is 2000s, 70...99 1900s, if n =3D=3D 0 keep=\r
+ unset */\r
+> > +static int\r
+> > +expand_year (unsigned long year, size_t n)\r
+> > +{\r
+> > +    if (n =3D=3D 2) {\r
+> > +  return (year < 70 ? 2000 : 1900) + year;\r
+> > +    } else if (n =3D=3D 4) {\r
+> > +  return year;\r
+> > +    } else {\r
+> > +  return UNSET;\r
+> > +    }\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +parse_date (struct state *state, char sep,\r
+> > +      unsigned long v1, unsigned long v2, unsigned long v3,\r
+> > +      size_t n1, size_t n2, size_t n3)\r
+> > +{\r
+> > +    int year =3D UNSET, mon =3D UNSET, mday =3D UNSET;\r
+> > +\r
+> > +    assert (is_date_sep (sep));\r
+> > +\r
+> > +    switch (sep) {\r
+> > +    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */\r
+> > +  if (n1 !=3D 1 && n1 !=3D 2)\r
+> > +      return -PARSE_TIME_ERR_DATEFORMAT;\r
+> > +\r
+> > +  if ((n2 =3D=3D 1 || n2 =3D=3D 2) && (n3 =3D=3D 0 || n3 =3D=3D 2 || n3=\r
+ =3D=3D 4)) {\r
+> > +      /* M[M]/D[D][/YY[YY]] */\r
+> > +      year =3D expand_year (v3, n3);\r
+> > +      mon =3D v1;\r
+> > +      mday =3D v2;\r
+> > +  } else if (n2 =3D=3D 4 && n3 =3D=3D 0) {\r
+> > +      /* M[M]/YYYY */\r
+> > +      year =3D v2;\r
+> > +      mon =3D v1;\r
+> > +  } else {\r
+> > +      return -PARSE_TIME_ERR_DATEFORMAT;\r
+> > +  }\r
+> > +  break;\r
+> > +\r
+> > +    case '-': /* Date: YYYY-MM[-DD] or DD-MM[-YY[YY]] or MM-YYYY */\r
+> > +  if (n1 =3D=3D 4 && n2 =3D=3D 2 && (n3 =3D=3D 0 || n3 =3D=3D 2)) {\r
+> > +      /* YYYY-MM[-DD] */\r
+> > +      year =3D v1;\r
+> > +      mon =3D v2;\r
+> > +      if (n3)\r
+> > +          mday =3D v3;\r
+> > +  } else if (n1 =3D=3D 2 && n2 =3D=3D 2 && (n3 =3D=3D 0 || n3 =3D=3D 2 =\r
+|| n3 =3D=3D 4)) {\r
+> > +      /* DD-MM[-YY[YY]] */\r
+> > +      year =3D expand_year (v3, n3);\r
+> > +      mon =3D v2;\r
+> > +      mday =3D v1;\r
+> > +  } else if (n1 =3D=3D 2 && n2 =3D=3D 4 && n3 =3D=3D 0) {\r
+> > +      /* MM-YYYY */\r
+> > +      year =3D v2;\r
+> > +      mon =3D v1;\r
+> > +  } else {\r
+> > +      return -PARSE_TIME_ERR_DATEFORMAT;\r
+> > +  }\r
+> > +  break;\r
+> > +\r
+> > +    case '.': /* Date: D[D].M[M][.[YY[YY]]] */\r
+> > +  if ((n1 !=3D 1 && n1 !=3D 2) || (n2 !=3D 1 && n2 !=3D 2) ||\r
+> > +      (n3 !=3D 0 && n3 !=3D 2 && n3 !=3D 4))\r
+> > +      return -PARSE_TIME_ERR_DATEFORMAT;\r
+> > +\r
+> > +  year =3D expand_year (v3, n3);\r
+> > +  mon =3D v2;\r
+> > +  mday =3D v1;\r
+> > +  break;\r
+> > +    }\r
+> > +\r
+> > +    if (year !=3D UNSET && year < 1970)\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +\r
+> > +    if (mon !=3D UNSET && (mon < 1 || mon > 12))\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +\r
+> > +    if (mday !=3D UNSET && (mday < 1 || mday > 31))\r
+> > +  return -PARSE_TIME_ERR_INVALIDDATE;\r
+> > +\r
+> > +    return set_abs_date (state, year, mon, mday);\r
+> > +}\r
+> > +\r
+> > +static int\r
+> > +parse_time (struct state *state, char sep,\r
+> > +      unsigned long v1, unsigned long v2, unsigned long v3,\r
+> > +      size_t n1, size_t n2, size_t n3)\r
+> > +{\r
+> > +    assert (is_time_sep (sep));\r
+> > +\r
+> > +    if ((n1 !=3D 1 && n1 !=3D 2) || n2 !=3D 2 || (n3 !=3D 0 && n3 !=3D=\r
+ 2))\r
+> > +  return -PARSE_TIME_ERR_TIMEFORMAT;\r
+> > +\r
+> > +    /*\r
+> > +     * REVISIT: this means it's required to set time *before* being\r
+> > +     * able to set timezone\r
+> > +     */\r
+> > +    if (is_field_set (state, TM_ABS_HOUR) &&\r
+> > +  is_field_set (state, TM_ABS_MIN) &&\r
+> > +  n1 =3D=3D 2 && n2 =3D=3D 2 && n3 =3D=3D 0 &&\r
+> > +  (state->delim =3D=3D '+' || state->delim =3D=3D '-')) {\r
+> > +  return set_user_tz (state, state->delim, v1, v2);\r
+> > +    }\r
+> > +\r
+> > +    if (v1 > 24 || v2 > 60 || v3 > 60)\r
+> > +  return -PARSE_TIME_ERR_INVALIDTIME;\r
+>=20\r
+> Are the > rather than >=3D deliberate here (i.e. do you mean to allow 60\r
+> for minutes or seconds)?\r
+\r
+For hours it's deliberate (24:00 is the end of day), for minutes and\r
+seconds it's accidental. Fixed.\r
+\r
+>=20\r
+> > +\r
+> > +    return set_abs_time (state, v1, v2, n3 ? v3 : 0);\r
+> > +}\r
+> > +\r
+> > +/* strtoul helper that assigns length */\r
+> > +static unsigned long\r
+> > +strtoul_len (const char *s, const char **endp, size_t *len)\r
+> > +{\r
+> > +    unsigned long val =3D strtoul (s, (char **) endp, 10);\r
+> > +\r
+> > +    *len =3D *endp - s;\r
+> > +    return val;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Parse a (group of) number(s). Return < 0 on error, number of parsed\r
+> > + * chars on success.\r
+> > + */\r
+> > +static ssize_t\r
+> > +parse_number (struct state *state, const char *s)\r
+> > +{\r
+> > +    int r;\r
+> > +    unsigned long v1, v2, v3 =3D 0;\r
+> > +    size_t n1, n2, n3 =3D 0;\r
+> > +    const char *p =3D s;\r
+> > +    char sep;\r
+> > +\r
+> > +    v1 =3D strtoul_len (p, &p, &n1);\r
+> > +\r
+> > +    if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {\r
+> > +  sep =3D *p;\r
+> > +  v2 =3D strtoul_len (p + 1, &p, &n2);\r
+> > +    } else {\r
+> > +  /* a single number */\r
+> > +  r =3D parse_single_number (state, v1, n1);\r
+> > +  if (r)\r
+> > +      return r;\r
+> > +\r
+> > +  return p - s;\r
+> > +    }\r
+> > +\r
+> > +    /* a group of two or three numbers? */\r
+> > +    if (*p =3D=3D sep && isdigit ((unsigned char) *(p + 1)))\r
+> > +  v3 =3D strtoul_len (p + 1, &p, &n3);\r
+> > +\r
+> > +    if (is_time_sep (sep))\r
+> > +  r =3D parse_time (state, sep, v1, v2, v3, n1, n2, n3);\r
+> > +    else\r
+> > +  r =3D parse_date (state, sep, v1, v2, v3, n1, n2, n3);\r
+> > +\r
+> > +    if (r)\r
+> > +  return r;\r
+> > +\r
+> > +    return p - s;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Parse delimiter(s). Return < 0 on error, number of parsed chars on\r
+> > + * success.\r
+> > + */\r
+> > +static ssize_t\r
+> > +parse_delim (struct state *state, const char *s)\r
+> > +{\r
+> > +    const char *p =3D s;\r
+> > +\r
+> > +    /*\r
+> > +     * REVISIT: any actions depending on the first delim after last\r
+> > +     * field? what could it be?\r
+> > +     */\r
+> > +\r
+> > +    /*\r
+> > +     * skip non-alpha and non-digit, and store the last for further\r
+> > +     * processing\r
+> > +     */\r
+> > +    while (*p && !isalnum ((unsigned char) *p)) {\r
+> > +  set_delim (state, *p);\r
+> > +  p++;\r
+> > +    }\r
+> > +\r
+> > +    return p - s;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Parse a date/time string. Return < 0 on error, number of parsed\r
+> > + * chars on success.\r
+> > + */\r
+> > +static ssize_t\r
+> > +parse_input (struct state *state, const char *s)\r
+> > +{\r
+> > +    const char *p =3D s;\r
+> > +    ssize_t n;\r
+> > +    int r;\r
+> > +\r
+> > +    while (*p) {\r
+> > +  if (isalpha ((unsigned char) *p)) {\r
+> > +      n =3D parse_keyword (state, p);\r
+> > +  } else if (isdigit ((unsigned char) *p)) {\r
+> > +      n =3D parse_number (state, p);\r
+> > +  } else {\r
+> > +      n =3D parse_delim (state, p);\r
+> > +  }\r
+> > +\r
+> > +  if (n <=3D 0) {\r
+> > +      if (n =3D=3D 0)\r
+> > +          n =3D -PARSE_TIME_ERR;\r
+> > +\r
+> > +      return n;             /* FIXME */\r
+> > +  }\r
+> > +\r
+> > +  p +=3D n;\r
+> > +    }\r
+> > +\r
+> > +    /* parse postponed number, if any */\r
+> > +    r =3D handle_postponed_number (state);\r
+> > +    if (r < 0)\r
+> > +  return r;\r
+> > +\r
+> > +    return p - s;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Processing the parsed input.\r
+> > + */\r
+> > +\r
+> > +/*\r
+> > + * Initialize reference time to tm. Use time zone in state if\r
+> > + * specified, otherwise local time. Use now for reference time if\r
+> > + * non-NULL, otherwise current time.\r
+> > + */\r
+> > +static int\r
+> > +initialize_now (struct state *state, struct tm *tm, const time_t *now)\r
+> > +{\r
+> > +    time_t t;\r
+> > +\r
+> > +    if (now) {\r
+> > +  t =3D *now;\r
+> > +    } else {\r
+> > +  if (time (&t) =3D=3D (time_t) -1)\r
+> > +      return -PARSE_TIME_ERR_LIB;\r
+> > +    }\r
+> > +\r
+> > +    if (is_field_set (state, TM_TZ)) {\r
+> > +  /* some other time zone */\r
+> > +\r
+> > +  /* adjust now according to the TZ */\r
+> > +  t +=3D get_field (state, TM_TZ) * 60;\r
+> > +\r
+> > +  /* it's not gm, but this doesn't mess with the tz */\r
+> > +  if (gmtime_r (&t, tm) =3D=3D NULL)\r
+> > +      return -PARSE_TIME_ERR_LIB;\r
+> > +    } else {\r
+> > +  /* local time */\r
+> > +  if (localtime_r (&t, tm) =3D=3D NULL)\r
+> > +      return -PARSE_TIME_ERR_LIB;\r
+> > +    }\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/*\r
+> > + * Normalize tm according to mktime(3). Both mktime(3) and\r
+> > + * localtime_r(3) use local time, but they cancel each other out here,\r
+> > + * making this function agnostic to time zone.\r
+> > + */\r
+> > +static int\r
+> > +normalize_tm (struct tm *tm)\r
+> > +{\r
+> > +    time_t t =3D mktime (tm);\r
+> > +\r
+> > +    if (t =3D=3D (time_t) -1)\r
+> > +  return -PARSE_TIME_ERR_LIB;\r
+> > +\r
+> > +    if (!localtime_r (&t, tm))\r
+> > +  return -PARSE_TIME_ERR_LIB;\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/* Get field out of a struct tm. */\r
+> > +static int\r
+> > +tm_get_field (const struct tm *tm, enum field field)\r
+> > +{\r
+> > +    switch (field) {\r
+> > +    case TM_ABS_SEC:      return tm->tm_sec;\r
+> > +    case TM_ABS_MIN:      return tm->tm_min;\r
+> > +    case TM_ABS_HOUR:     return tm->tm_hour;\r
+> > +    case TM_ABS_MDAY:     return tm->tm_mday;\r
+> > +    case TM_ABS_MON:      return tm->tm_mon + 1; /* 0- to 1-based */\r
+> > +    case TM_ABS_YEAR:     return 1900 + tm->tm_year;\r
+> > +    case TM_ABS_WDAY:     return tm->tm_wday;\r
+> > +    case TM_ABS_ISDST:    return tm->tm_isdst;\r
+> > +    default:\r
+> > +  assert (false);\r
+> > +  break;\r
+> > +    }\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/* Modify hour according to am/pm setting. */\r
+> > +static int\r
+> > +fixup_ampm (struct state *state)\r
+> > +{\r
+> > +    int hour, hdiff =3D 0;\r
+> > +\r
+> > +    if (!is_field_set (state, TM_AMPM))\r
+> > +  return 0;\r
+> > +\r
+> > +    if (!is_field_set (state, TM_ABS_HOUR))\r
+> > +  return -PARSE_TIME_ERR_TIMEFORMAT;\r
+> > +\r
+> > +    hour =3D get_field (state, TM_ABS_HOUR);\r
+> > +    if (hour < 1 || hour > 12)\r
+> > +  return -PARSE_TIME_ERR_INVALIDTIME;\r
+> > +\r
+> > +    if (get_field (state, TM_AMPM)) {\r
+> > +  /* 12pm is noon */\r
+> > +  if (hour !=3D 12)\r
+> > +      hdiff =3D 12;\r
+> > +    } else {\r
+> > +  /* 12am is midnight, beginning of day */\r
+> > +  if (hour =3D=3D 12)\r
+> > +      hdiff =3D -12;\r
+> > +    }\r
+> > +\r
+> > +    mod_field (state, TM_REL_HOUR, -hdiff);\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/* Combine absolute and relative fields, and round. */\r
+> > +static int\r
+> > +create_output (struct state *state, time_t *t_out, const time_t *tnow,\r
+> > +         int round)\r
+> > +{\r
+> > +    struct tm tm =3D { 0 };\r
+> > +    struct tm now;\r
+> > +    enum field f;\r
+> > +    int r;\r
+> > +    int week_round =3D PARSE_TIME_NO_ROUND;\r
+> > +\r
+> > +    r =3D initialize_now (state, &now, tnow);\r
+> > +    if (r)\r
+> > +  return r;\r
+> > +\r
+> > +    /* initialize uninitialized fields to now */\r
+> > +    for (f =3D TM_ABS_SEC; f !=3D TM_NONE; f =3D next_field (f)) {\r
+> > +  if (state->set[f] =3D=3D FIELD_NOW) {\r
+> > +      state->tm[f] =3D tm_get_field (&now, f);\r
+> > +      state->set[f] =3D FIELD_SET;\r
+> > +  }\r
+> > +    }\r
+> > +\r
+> > +    /*\r
+> > +     * If MON is set but YEAR is not, refer to past month.\r
+> > +     *\r
+> > +     * REVISIT: Why are month/week special in this regard? What about\r
+> > +     * mday, or time. Should refer to past.\r
+> > +     */\r
+> > +    if (is_field_set (state, TM_ABS_MON) &&\r
+> > +  !is_field_set (state, TM_ABS_YEAR)) {\r
+> > +  if (get_field (state, TM_ABS_MON) >=3D tm_get_field (&now, TM_ABS_MON=\r
+))\r
+> > +      mod_field (state, TM_REL_YEAR, 1);\r
+> > +    }\r
+> > +\r
+> > +    /*\r
+> > +     * If WDAY is set but MDAY is not, we consider WDAY relative\r
+> > +     *\r
+> > +     * REVISIT: This fails on stuff like "two months ago monday"\r
+> > +     * because two months ago wasn't the same day as today. Postpone\r
+> > +     * until we know date?\r
+> > +     */\r
+> > +    if (is_field_set (state, TM_ABS_WDAY) &&\r
+> > +  !is_field_set (state, TM_ABS_MDAY)) {\r
+> > +  int wday =3D get_field (state, TM_ABS_WDAY);\r
+> > +  int today =3D tm_get_field (&now, TM_ABS_WDAY);\r
+> > +  int rel_days;\r
+> > +\r
+> > +  if (today > wday)\r
+> > +      rel_days =3D today - wday;\r
+> > +  else\r
+> > +      rel_days =3D today + 7 - wday;\r
+> > +\r
+> > +  /* this also prevents special week rounding from happening */\r
+> > +  mod_field (state, TM_REL_DAY, rel_days);\r
+> > +\r
+> > +  unset_field (state, TM_ABS_WDAY);\r
+> > +    }\r
+> > +\r
+> > +    r =3D fixup_ampm (state);\r
+> > +    if (r)\r
+> > +  return r;\r
+> > +\r
+> > +    /*\r
+> > +     * Iterate fields from least accurate to most accurate, and set\r
+> > +     * unset fields according to requested rounding.\r
+> > +     */\r
+> > +    for (f =3D TM_ABS_SEC; f !=3D TM_NONE; f =3D next_field (f)) {\r
+> > +  if (round !=3D PARSE_TIME_NO_ROUND) {\r
+> > +      enum field r =3D abs_to_rel_field (f);\r
+>=20\r
+> The comment and the code seem to disagree on the ordering.\r
+\r
+Thanks, fixed.\r
+\r
+>=20\r
+> > +\r
+> > +      if (is_field_set (state, f) || is_field_set (state, r)) {\r
+> > +          if (round >=3D PARSE_TIME_ROUND_UP)\r
+> > +              mod_field (state, r, -1);\r
+> > +          round =3D PARSE_TIME_NO_ROUND; /* no more rounding */\r
+> > +      } else {\r
+> > +          if (f =3D=3D TM_ABS_MDAY &&\r
+> > +              is_field_set (state, TM_REL_WEEK)) {\r
+> > +              /* week is most accurate */\r
+> > +              week_round =3D round;\r
+> > +              round =3D PARSE_TIME_NO_ROUND;\r
+> > +          } else {\r
+> > +              set_field (state, f, field_zero (f));\r
+> > +          }\r
+> > +      }\r
+> > +  }\r
+> > +\r
+> > +  if (!is_field_set (state, f))\r
+> > +      set_field (state, f, tm_get_field (&now, f));\r
+> > +    }\r
+> > +\r
+> > +    /* special case: rounding with week accuracy */\r
+> > +    if (week_round !=3D PARSE_TIME_NO_ROUND) {\r
+> > +  /* temporarily set more accurate fields to now */\r
+> > +  set_field (state, TM_ABS_SEC, tm_get_field (&now, TM_ABS_SEC));\r
+> > +  set_field (state, TM_ABS_MIN, tm_get_field (&now, TM_ABS_MIN));\r
+> > +  set_field (state, TM_ABS_HOUR, tm_get_field (&now, TM_ABS_HOUR));\r
+> > +  set_field (state, TM_ABS_MDAY, tm_get_field (&now, TM_ABS_MDAY));\r
+> > +    }\r
+> > +\r
+> > +    /*\r
+> > +     * set all fields. they may contain out of range values before\r
+> > +     * normalization by mktime(3).\r
+> > +     */\r
+> > +    tm.tm_sec =3D get_field (state, TM_ABS_SEC) - get_field (state, TM=\r
+_REL_SEC);\r
+> > +    tm.tm_min =3D get_field (state, TM_ABS_MIN) - get_field (state, TM=\r
+_REL_MIN);\r
+> > +    tm.tm_hour =3D get_field (state, TM_ABS_HOUR) - get_field (state, =\r
+TM_REL_HOUR);\r
+> > +    tm.tm_mday =3D get_field (state, TM_ABS_MDAY) -\r
+> > +           get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK);\r
+> > +    tm.tm_mon =3D get_field (state, TM_ABS_MON) - get_field (state, TM=\r
+_REL_MON);\r
+> > +    tm.tm_mon--; /* 1- to 0-based */\r
+> > +    tm.tm_year =3D get_field (state, TM_ABS_YEAR) - get_field (state, =\r
+TM_REL_YEAR) - 1900;\r
+> > +\r
+> > +    /*\r
+> > +     * It's always normal time.\r
+> > +     *\r
+> > +     * REVISIT: This is probably not a solution that universally\r
+> > +     * works. Just make sure DST is not taken into account. We don't\r
+> > +     * want rounding to be affected by DST.\r
+> > +     */\r
+> > +    tm.tm_isdst =3D -1;\r
+> > +\r
+> > +    /* special case: rounding with week accuracy */\r
+> > +    if (week_round !=3D PARSE_TIME_NO_ROUND) {\r
+> > +  /* normalize to get proper tm.wday */\r
+> > +  r =3D normalize_tm (&tm);\r
+> > +  if (r < 0)\r
+> > +      return r;\r
+> > +\r
+> > +  /* set more accurate fields back to zero */\r
+> > +  tm.tm_sec =3D 0;\r
+> > +  tm.tm_min =3D 0;\r
+> > +  tm.tm_hour =3D 0;\r
+> > +  tm.tm_isdst =3D -1;\r
+> > +\r
+> > +  /* monday is the true 1st day of week, but this is easier */\r
+> > +  if (week_round <=3D PARSE_TIME_ROUND_DOWN)\r
+> > +      tm.tm_mday -=3D tm.tm_wday;\r
+> > +  else\r
+> > +      tm.tm_mday +=3D 7 - tm.tm_wday;\r
+> > +    }\r
+> > +\r
+> > +    /* if TZ specified, convert from TZ to local time for mktime(3) */\r
+> > +    if (is_field_set (state, TM_TZ)) {\r
+> > +  time_t t =3D mktime (&tm);\r
+> > +\r
+> > +  /* from specified TZ to UTC */\r
+> > +  tm.tm_min -=3D get_field (state, TM_TZ);\r
+> > +\r
+> > +  /* from UTC to local TZ (yes, it's hacky - FIXME) */\r
+> > +  tm.tm_sec +=3D difftime (mktime (localtime (&t)), mktime (gmtime (&t)=\r
+));\r
+> > +    }\r
+> > +\r
+> > +    /* FIXME: check return value, don't set if fail */\r
+> > +    *t_out =3D mktime (&tm);\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > +\r
+> > +/* internally, all errors are < 0. parse_time_string() returns errors =\r
+> 0. */\r
+> > +#define EXTERNAL_ERR(r) (-r)\r
+> > +\r
+> > +int\r
+> > +parse_time_string (const char *s, time_t *t, const time_t *now, int ro=\r
+und)\r
+> > +{\r
+> > +    struct state state =3D { { 0 } };\r
+> > +    int r;\r
+> > +\r
+> > +    if (!s || !t)\r
+> > +  return EXTERNAL_ERR (-PARSE_TIME_ERR);\r
+> > +\r
+> > +    r =3D parse_input (&state, s);\r
+> > +    if (r < 0)\r
+> > +  return EXTERNAL_ERR (r);\r
+> > +\r
+> > +    r =3D create_output (&state, t, now, round);\r
+> > +    if (r < 0)\r
+> > +  return EXTERNAL_ERR (r);\r
+> > +\r
+> > +    return 0;\r
+> > +}\r
+> > diff --git a/lib/parse-time-string.h b/lib/parse-time-string.h\r
+> > new file mode 100644\r
+> > index 0000000..50b7c6f\r
+> > --- /dev/null\r
+> > +++ b/lib/parse-time-string.h\r
+> > @@ -0,0 +1,95 @@\r
+> > +/*\r
+> > + * parse time string - user friendly date and time parser\r
+> > + * Copyright =C2=A9 2012 Jani Nikula\r
+> > + *\r
+> > + * This program is free software: you can redistribute it and/or modify\r
+> > + * it under the terms of the GNU General Public License as published by\r
+> > + * the Free Software Foundation, either version 2 of the License, or\r
+> > + * (at your option) any later version.\r
+> > + *\r
+> > + * This program is distributed in the hope that it will be useful,\r
+> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+> > + * GNU General Public License for more details.\r
+> > + *\r
+> > + * You should have received a copy of the GNU General Public License\r
+> > + * along with this program.  If not, see <http://www.gnu.org/licenses/=\r
+>.\r
+> > + *\r
+> > + * Author: Jani Nikula <jani@nikula.org>\r
+> > + */\r
+> > +\r
+> > +#ifndef PARSE_TIME_STRING_H\r
+> > +#define PARSE_TIME_STRING_H\r
+> > +\r
+> > +#ifdef __cplusplus\r
+> > +extern "C" {\r
+> > +#endif\r
+> > +\r
+> > +#include <time.h>\r
+> > +\r
+> > +/* return values for parse_time_string() */\r
+> > +enum {\r
+> > +    PARSE_TIME_OK =3D 0,\r
+> > +    PARSE_TIME_ERR,               /* unspecified error */\r
+> > +    PARSE_TIME_ERR_LIB,           /* library call failed */\r
+> > +    PARSE_TIME_ERR_ALREADYSET,    /* attempt to set unit twice */\r
+> > +    PARSE_TIME_ERR_FORMAT,        /* generic date/time format error */\r
+> > +    PARSE_TIME_ERR_DATEFORMAT,    /* date format error */\r
+> > +    PARSE_TIME_ERR_TIMEFORMAT,    /* time format error */\r
+> > +    PARSE_TIME_ERR_INVALIDDATE,   /* date value error */\r
+> > +    PARSE_TIME_ERR_INVALIDTIME,   /* time value error */\r
+> > +    PARSE_TIME_ERR_KEYWORD,       /* unknown keyword */\r
+> > +};\r
+> > +\r
+> > +/* round values for parse_time_string() */\r
+> > +enum {\r
+> > +    PARSE_TIME_ROUND_DOWN =3D -1,\r
+> > +    PARSE_TIME_NO_ROUND =3D 0,\r
+> > +    PARSE_TIME_ROUND_UP =3D 1,\r
+> > +};\r
+> > +\r
+> > +/**\r
+> > + * parse_time_string() - user friendly date and time parser\r
+> > + * @s:            string to parse\r
+> > + * @t:            pointer to time_t to store parsed time in\r
+> > + * @now:  pointer to time_t containing reference date/time, or NULL\r
+> > + * @round:        PARSE_TIME_NO_ROUND, PARSE_TIME_ROUND_DOWN, or\r
+> > + *                PARSE_TIME_ROUND_UP\r
+> > + *\r
+> > + * Parse a date/time string 's' and store the parsed date/time result\r
+> > + * in 't'.\r
+> > + *\r
+> > + * A reference date/time is used for determining the "date/time units"\r
+> > + * (roughly equivalent to struct tm members) not specified by 's'. If\r
+> > + * 'now' is non-NULL, it must contain a pointer to a time_t to be used\r
+> > + * as reference date/time. Otherwise, the current time is used.\r
+> > + *\r
+> > + * If 's' does not specify a full date/time, the 'round' parameter\r
+> > + * specifies if and how the result should be rounded as follows:\r
+> > + *\r
+> > + *   PARSE_TIME_NO_ROUND: All date/time units that are not specified\r
+> > + *   by 's' are set to the corresponding unit derived from the\r
+> > + *   reference date/time.\r
+> > + *\r
+> > + *   PARSE_TIME_ROUND_DOWN: All date/time units that are more accurate\r
+> > + *   than the most accurate unit specified by 's' are set to the\r
+> > + *   smallest valid value for that unit. Rest of the unspecified units\r
+> > + *   are set as in PARSE_TIME_NO_ROUND.\r
+> > + *\r
+> > + *   PARSE_TIME_ROUND_UP: All date/time units that are more accurate\r
+> > + *   than the most accurate unit specified by 's' are set to the\r
+> > + *   smallest valid value for that unit. The most accurate unit\r
+> > + *   specified by 's' is incremented by one (and this is rolled over\r
+> > + *   to the less accurate units as necessary). Rest of the unspecified\r
+> > + *   units are set as in PARSE_TIME_NO_ROUND.\r
+> > + *\r
+> > + * Return 0 (PARSE_TIME_OK) for succesfully parsed date/time, or one\r
+> > + * of PARSE_TIME_ERR_* on error. 't' is not modified on error.\r
+> > + */\r
+> > +int parse_time_string (const char *s, time_t *t, const time_t *now, in=\r
+t round);\r
+> > +\r
+> > +#ifdef __cplusplus\r
+> > +}\r
+> > +#endif\r
+> > +\r
+> > +#endif /* PARSE_TIME_STRING_H */\r
+> > --=20\r
+> > 1.7.5.4\r
+> >=20\r
+> > _______________________________________________\r
+> > notmuch mailing list\r
+> > notmuch@notmuchmail.org\r
+> > http://notmuchmail.org/mailman/listinfo/notmuch\r