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