[PATCH 2/7] lib: add a date/time parser module
authorJani Nikula <jani@nikula.org>
Fri, 3 Aug 2012 21:51:47 +0000 (00:51 +0300)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:48:48 +0000 (09:48 -0800)
ad/7e56a21456574fd1b3f6be113f220c397912a0 [new file with mode: 0644]

diff --git a/ad/7e56a21456574fd1b3f6be113f220c397912a0 b/ad/7e56a21456574fd1b3f6be113f220c397912a0
new file mode 100644 (file)
index 0000000..98575f3
--- /dev/null
@@ -0,0 +1,1590 @@
+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 23109431FAF\r
+       for <notmuch@notmuchmail.org>; Fri,  3 Aug 2012 14:52:16 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Amavis-Alert: BAD HEADER SECTION, Duplicate header field: "References"\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 01JnpU6F6f7w for <notmuch@notmuchmail.org>;\r
+       Fri,  3 Aug 2012 14:52:06 -0700 (PDT)\r
+Received: from mail-lb0-f181.google.com (mail-lb0-f181.google.com\r
+       [209.85.217.181]) (using TLSv1 with cipher RC4-SHA (128/128 bits))\r
+       (No client certificate requested)\r
+       by olra.theworths.org (Postfix) with ESMTPS id C5FC0431FBD\r
+       for <notmuch@notmuchmail.org>; Fri,  3 Aug 2012 14:52:04 -0700 (PDT)\r
+Received: by lbbgk1 with SMTP id gk1so621084lbb.26\r
+       for <notmuch@notmuchmail.org>; Fri, 03 Aug 2012 14:52:03 -0700 (PDT)\r
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r
+       d=google.com; s=20120113;\r
+       h=from:to:cc:subject:date:message-id:x-mailer:in-reply-to:references\r
+       :in-reply-to:references:mime-version:content-type\r
+       :content-transfer-encoding:x-gm-message-state;\r
+       bh=Zn4xSNUB0JOswzcv1FR65oHsh2zpWZtJb9uVNauYiGE=;\r
+       b=gSYBRPwPsH9kH4900855ektw3yIYB0WjrQqcFpqlcDQLt88NGvc0R5d4/r8+XcgzHM\r
+       6IBCyjL+EtY6/Xan1gFhsxtf6peUgVuWKMaRGEx/RsScXLGstR2anBqyzQv8WK0yNbgC\r
+       fMACT+g7fpzUVPSkmarF7mPyS41txw74XA8mK/5BCOkg6Qln4syIMgh9HgY9WGGHly0L\r
+       nPkXYuW0fc15e4sI0rgJQUpZLhQGrPXkGMQp9ElI2YPgv+X1NGWeREd8dK/xZra18DBU\r
+       37GKBDWzVAsBaezSNzaihQLy/uwJ1HifSvSo1B8MhC7eUJT0uTV+QoyzuAzTncZL0OjT\r
+       9dHg==\r
+Received: by 10.112.42.225 with SMTP id r1mr1198534lbl.102.1344030722999;\r
+       Fri, 03 Aug 2012 14:52:02 -0700 (PDT)\r
+Received: from localhost (dsl-hkibrasgw4-fe51df00-27.dhcp.inet.fi.\r
+       [80.223.81.27])\r
+       by mx.google.com with ESMTPS id q8sm2258216lbj.2.2012.08.03.14.52.00\r
+       (version=SSLv3 cipher=OTHER); Fri, 03 Aug 2012 14:52:02 -0700 (PDT)\r
+From: Jani Nikula <jani@nikula.org>\r
+To: notmuch@notmuchmail.org\r
+Subject: [PATCH 2/7] lib: add a date/time parser module\r
+Date: Sat,  4 Aug 2012 00:51:47 +0300\r
+Message-Id:\r
+ <615d95753dbe44b1f975ad8c1c46ee33d03d04db.1344028781.git.jani@nikula.org>\r
+X-Mailer: git-send-email 1.7.9.5\r
+In-Reply-To: <cover.1344028781.git.jani@nikula.org>\r
+References: <cover.1344028781.git.jani@nikula.org>\r
+In-Reply-To: <cover.1344028781.git.jani@nikula.org>\r
+References: <cover.1344028781.git.jani@nikula.org>\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=UTF-8\r
+Content-Transfer-Encoding: 8bit\r
+X-Gm-Message-State:\r
+ ALoCoQmeyETh5xTgwphsVPuIoo32v7ySTGafuBcCl4SbXlvxpvgN4FNSkBiG4ENhDR9Boj3pAsd6\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: Fri, 03 Aug 2012 21:52:16 -0000\r
+\r
+Build a date/time parser as part of the notmuch lib, to be used for\r
+adding date range query support later on.\r
+\r
+Signed-off-by: Jani Nikula <jani@nikula.org>\r
+---\r
+ lib/Makefile.local      |    1 +\r
+ lib/parse-time-string.c | 1384 +++++++++++++++++++++++++++++++++++++++++++++++\r
+ lib/parse-time-string.h |   95 ++++\r
+ 3 files changed, 1480 insertions(+)\r
+ create mode 100644 lib/parse-time-string.c\r
+ create mode 100644 lib/parse-time-string.h\r
+\r
+diff --git a/lib/Makefile.local b/lib/Makefile.local\r
+index 8a9aa28..e29c3a2 100644\r
+--- a/lib/Makefile.local\r
++++ b/lib/Makefile.local\r
+@@ -53,6 +53,7 @@ libnotmuch_c_srcs =          \\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
\r
+diff --git a/lib/parse-time-string.c b/lib/parse-time-string.c\r
+new file mode 100644\r
+index 0000000..7c50f3e\r
+--- /dev/null\r
++++ b/lib/parse-time-string.c\r
+@@ -0,0 +1,1384 @@\r
++/*\r
++ * parse time string - user friendly date and time parser\r
++ * Copyright © 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
++ * Author: Jani Nikula <jani@nikula.org>\r
++ */\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 unused(x) x __attribute__ ((unused))\r
++\r
++/* REVISIT: Redefine these to add i18n support. The keyword table uses\r
++ * N_() to mark strings to be translated; they are accessed\r
++ * dynamically using _(). */\r
++#define _(s) (s)      /* i18n: define as gettext (s) */\r
++#define N_(s) (s)     /* i18n: define as gettext_noop (s) */\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 = 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 <= 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 == TM_ABS_MDAY || field == TM_ABS_MON)\r
++      return 1;\r
++    else if (field == 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
++    char postponed_delim;\r
++};\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, char *d)\r
++{\r
++    if (!state->postponed_length)\r
++      return false;\r
++\r
++    if (n)\r
++      *n = state->postponed_length;\r
++\r
++    if (v)\r
++      *v = state->postponed_value;\r
++\r
++    if (d)\r
++      *d = state->postponed_delim;\r
++\r
++    state->postponed_length = 0;\r
++    state->postponed_value = 0;\r
++    state->postponed_delim = 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, char d);\r
++static int\r
++handle_postponed_number (struct state *state)\r
++{\r
++    int v = state->postponed_value;\r
++    int n = state->postponed_length;\r
++    char d = state->postponed_delim;\r
++\r
++    if (!n)\r
++      return 0;\r
++\r
++    state->postponed_value = 0;\r
++    state->postponed_length = 0;\r
++    state->postponed_delim = 0;\r
++\r
++    return parse_postponed_number (state, v, n, d);\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
++    char d = state->delim;\r
++\r
++    /* parse previous postponed number, if any */\r
++    r = handle_postponed_number (state);\r
++    if (r)\r
++      return r;\r
++\r
++    state->postponed_length = n;\r
++    state->postponed_value = v;\r
++    state->postponed_delim = d;\r
++\r
++    return 0;\r
++}\r
++\r
++static void\r
++set_delim (struct state *state, char delim)\r
++{\r
++    state->delim = delim;\r
++}\r
++\r
++static void\r
++unset_delim (struct state *state)\r
++{\r
++    state->delim = 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] != 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] = FIELD_UNSET;\r
++    state->tm[field] = 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] != FIELD_UNSET)\r
++      return -PARSE_TIME_ERR_ALREADYSET;\r
++\r
++    state->set[field] = 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 = handle_postponed_number (state);\r
++    if (r)\r
++      return r;\r
++\r
++    unset_delim (state);\r
++\r
++    state->tm[field] = value;\r
++    state->last_field = 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 = 0; i < n; i++) {\r
++      r = set_field (state, fields[i], 0);\r
++      if (r)\r
++          return r;\r
++      state->set[fields[i]] = FIELD_NOW;\r
++    }\r
++\r
++    return 0;\r
++}\r
++\r
++/* Modify field by adding value to it. To be used on relative fields. */\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] = FIELD_SET;\r
++\r
++    /* parse postponed number, if any */\r
++    r = handle_postponed_number (state);\r
++    if (r)\r
++      return r;\r
++\r
++    unset_delim (state);\r
++\r
++    state->tm[field] += value;\r
++    state->last_field = 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
++/*\r
++ * Validity checkers.\r
++ */\r
++static bool is_valid_12hour (int h)\r
++{\r
++    return h >= 0 && h <= 12;\r
++}\r
++\r
++static bool is_valid_time (int h, int m, int s)\r
++{\r
++    /* allow 24:00:00 to denote end of day */\r
++    if (h == 24 && m == 0 && s == 0)\r
++      return true;\r
++\r
++    return h >= 0 && h <= 23 && m >= 0 && m <= 59 && s >= 0 && s <= 59;\r
++}\r
++\r
++static bool is_valid_mday (int mday)\r
++{\r
++    return mday >= 1 && mday <= 31;\r
++}\r
++\r
++static bool is_valid_mon (int mon)\r
++{\r
++    return mon >= 1 && mon <= 12;\r
++}\r
++\r
++static bool is_valid_year (int year)\r
++{\r
++    return year >= 1970;\r
++}\r
++\r
++static bool is_valid_date (int year, int mon, int mday)\r
++{\r
++    return is_valid_year (year) && is_valid_mon (mon) && is_valid_mday (mday);\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
++static int\r
++set_abs_time (struct state *state, int hour, int min, int sec)\r
++{\r
++    int r;\r
++\r
++    if (hour != UNSET) {\r
++      if ((r = set_field (state, TM_ABS_HOUR, hour)))\r
++          return r;\r
++    }\r
++\r
++    if (min != UNSET) {\r
++      if ((r = set_field (state, TM_ABS_MIN, min)))\r
++          return r;\r
++    }\r
++\r
++    if (sec != UNSET) {\r
++      if ((r = 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
++static int\r
++set_abs_date (struct state *state, int year, int mon, int mday)\r
++{\r
++    int r;\r
++\r
++    if (year != UNSET) {\r
++      if ((r = set_field (state, TM_ABS_YEAR, year)))\r
++          return r;\r
++    }\r
++\r
++    if (mon != UNSET) {\r
++      if ((r = set_field (state, TM_ABS_MON, mon)))\r
++          return r;\r
++    }\r
++\r
++    if (mday != UNSET) {\r
++      if ((r = 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
++    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 = 1;\r
++\r
++    /* get a previously set multiplier, if any */\r
++    get_postponed_number (state, &multiplier, NULL, 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 = 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 = get_postponed_length (state);\r
++\r
++    /* consume postponed number if it could be mday */\r
++    if (n == 1 || n == 2) {\r
++      int r, v;\r
++\r
++      get_postponed_number (state, &v, NULL, NULL);\r
++\r
++      if (!is_valid_mday (v))\r
++          return -PARSE_TIME_ERR_INVALIDDATE;\r
++\r
++      r = 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 = get_postponed_length (state);\r
++\r
++    /* consume postponed number if it could be hour */\r
++    if (n == 1 || n == 2) {\r
++      int r, v;\r
++\r
++      get_postponed_number (state, &v, NULL, NULL);\r
++\r
++      if (!is_valid_12hour (v))\r
++          return -PARSE_TIME_ERR_INVALIDTIME;\r
++\r
++      r = 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, unused (struct keyword *kw))\r
++{\r
++    enum field fields[] = { 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, unused (struct keyword *kw))\r
++{\r
++    enum field fields[] = { 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, NULL))\r
++      return -PARSE_TIME_ERR_DATEFORMAT;\r
++\r
++    /* ordinals are mday */\r
++    if (n != 1 && n != 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") == 0 && v != 1 && v != 21 && v != 31)\r
++      return -PARSE_TIME_ERR_INVALIDDATE;\r
++    else if (strcasecmp (kw->name, "nd") == 0 && v != 2 && v != 22)\r
++      return -PARSE_TIME_ERR_INVALIDDATE;\r
++    else if (strcasecmp (kw->name, "rd") == 0 && v != 3 && v != 23)\r
++      return -PARSE_TIME_ERR_INVALIDDATE;\r
++    else if (strcasecmp (kw->name, "th") == 0 && !is_valid_mday (v))\r
++      return -PARSE_TIME_ERR_INVALIDDATE;\r
++\r
++    return set_field (state, TM_ABS_MDAY, v);\r
++}\r
++\r
++/*\r
++ * Accepted keywords.\r
++ *\r
++ * A keyword may optionally contain a '|' to indicate the minimum\r
++ * match length. Without one, full match is required. It's advisable\r
++ * to keep the minimum match parts unique across all 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[] = {\r
++    /* weekdays */\r
++    { N_("sun|day"),  TM_ABS_WDAY,    0,      NULL },\r
++    { N_("mon|day"),  TM_ABS_WDAY,    1,      NULL },\r
++    { N_("tue|sday"), TM_ABS_WDAY,    2,      NULL },\r
++    { N_("wed|nesday"),       TM_ABS_WDAY,    3,      NULL },\r
++    { N_("thu|rsday"),        TM_ABS_WDAY,    4,      NULL },\r
++    { N_("fri|day"),  TM_ABS_WDAY,    5,      NULL },\r
++    { N_("sat|urday"),        TM_ABS_WDAY,    6,      NULL },\r
++\r
++    /* months */\r
++    { N_("jan|uary"), TM_ABS_MON,     1,      kw_set_month },\r
++    { N_("feb|ruary"),        TM_ABS_MON,     2,      kw_set_month },\r
++    { N_("mar|ch"),   TM_ABS_MON,     3,      kw_set_month },\r
++    { N_("apr|il"),   TM_ABS_MON,     4,      kw_set_month },\r
++    { N_("may"),      TM_ABS_MON,     5,      kw_set_month },\r
++    { N_("jun|e"),    TM_ABS_MON,     6,      kw_set_month },\r
++    { N_("jul|y"),    TM_ABS_MON,     7,      kw_set_month },\r
++    { N_("aug|ust"),  TM_ABS_MON,     8,      kw_set_month },\r
++    { N_("sep|tember"),       TM_ABS_MON,     9,      kw_set_month },\r
++    { N_("oct|ober"), TM_ABS_MON,     10,     kw_set_month },\r
++    { N_("nov|ember"),        TM_ABS_MON,     11,     kw_set_month },\r
++    { N_("dec|ember"),        TM_ABS_MON,     12,     kw_set_month },\r
++\r
++    /* durations */\r
++    { N_("y|ears"),   TM_REL_YEAR,    1,      kw_set_rel },\r
++    { N_("w|eeks"),   TM_REL_WEEK,    1,      kw_set_rel },\r
++    { N_("d|ays"),    TM_REL_DAY,     1,      kw_set_rel },\r
++    { N_("h|ours"),   TM_REL_HOUR,    1,      kw_set_rel },\r
++    { N_("hr|s"),     TM_REL_HOUR,    1,      kw_set_rel },\r
++    { N_("m|inutes"), TM_REL_MIN,     1,      kw_set_rel },\r
++    /* M=months, m=minutes */\r
++    { N_("M"),                TM_REL_MON,     1,      kw_set_rel },\r
++    { N_("mins"),     TM_REL_MIN,     1,      kw_set_rel },\r
++    { N_("mo|nths"),  TM_REL_MON,     1,      kw_set_rel },\r
++    { N_("s|econds"), TM_REL_SEC,     1,      kw_set_rel },\r
++    { N_("secs"),     TM_REL_SEC,     1,      kw_set_rel },\r
++\r
++    /* numbers */\r
++    { N_("one"),      TM_NONE,        1,      kw_set_number },\r
++    { N_("two"),      TM_NONE,        2,      kw_set_number },\r
++    { N_("three"),    TM_NONE,        3,      kw_set_number },\r
++    { N_("four"),     TM_NONE,        4,      kw_set_number },\r
++    { N_("five"),     TM_NONE,        5,      kw_set_number },\r
++    { N_("six"),      TM_NONE,        6,      kw_set_number },\r
++    { N_("seven"),    TM_NONE,        7,      kw_set_number },\r
++    { N_("eight"),    TM_NONE,        8,      kw_set_number },\r
++    { N_("nine"),     TM_NONE,        9,      kw_set_number },\r
++    { N_("ten"),      TM_NONE,        10,     kw_set_number },\r
++    { N_("dozen"),    TM_NONE,        12,     kw_set_number },\r
++    { N_("hundred"),  TM_NONE,        100,    kw_set_number },\r
++\r
++    /* special number forms */\r
++    { N_("this"),     TM_NONE,        0,      kw_set_number },\r
++    { N_("last"),     TM_NONE,        1,      kw_set_number },\r
++\r
++    /* specials */\r
++    { N_("yesterday"),        TM_REL_DAY,     1,      kw_set_rel },\r
++    { N_("today"),    TM_NONE,        0,      kw_set_today },\r
++    { N_("now"),      TM_NONE,        0,      kw_set_now },\r
++    { N_("noon"),     TM_NONE,        12,     kw_set_timeofday },\r
++    { N_("midnight"), TM_NONE,        0,      kw_set_timeofday },\r
++    { N_("am"),               TM_AMPM,        0,      kw_set_ampm },\r
++    { N_("a.m."),     TM_AMPM,        0,      kw_set_ampm },\r
++    { N_("pm"),               TM_AMPM,        1,      kw_set_ampm },\r
++    { N_("p.m."),     TM_AMPM,        1,      kw_set_ampm },\r
++    { N_("st"),               TM_NONE,        0,      kw_set_ordinal },\r
++    { N_("nd"),               TM_NONE,        0,      kw_set_ordinal },\r
++    { N_("rd"),               TM_NONE,        0,      kw_set_ordinal },\r
++    { N_("th"),               TM_NONE,        0,      kw_set_ordinal },\r
++\r
++    /* timezone codes: offset in minutes. FIXME: add more codes. */\r
++    { N_("pst"),      TM_TZ,          -8*60,  NULL },\r
++    { N_("mst"),      TM_TZ,          -7*60,  NULL },\r
++    { N_("cst"),      TM_TZ,          -6*60,  NULL },\r
++    { N_("est"),      TM_TZ,          -5*60,  NULL },\r
++    { N_("ast"),      TM_TZ,          -4*60,  NULL },\r
++    { N_("nst"),      TM_TZ,          -(3*60+30),     NULL },\r
++\r
++    { N_("gmt"),      TM_TZ,          0,      NULL },\r
++    { N_("utc"),      TM_TZ,          0,      NULL },\r
++\r
++    { N_("wet"),      TM_TZ,          0,      NULL },\r
++    { N_("cet"),      TM_TZ,          1*60,   NULL },\r
++    { N_("eet"),      TM_TZ,          2*60,   NULL },\r
++    { N_("fet"),      TM_TZ,          3*60,   NULL },\r
++\r
++    { N_("wat"),      TM_TZ,          1*60,   NULL },\r
++    { N_("cat"),      TM_TZ,          2*60,   NULL },\r
++    { N_("eat"),      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 == 0 all\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_case)\r
++{\r
++    size_t i;\r
++\r
++    for (i = 0; *s && *keyword; i++, s++, keyword++) {\r
++      if (match_case) {\r
++          if (*s != *keyword)\r
++              break;\r
++      } else {\r
++          if (tolower ((unsigned char) *s) !=\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 = 0;\r
++    struct keyword *kw = NULL;\r
++    int r;\r
++\r
++    /* Match longest keyword */\r
++    for (i = 0; i < ARRAY_SIZE (keywords); i++) {\r
++      /* Match case if keyword begins with upper case letter. */\r
++      bool mcase = isupper ((unsigned char) keywords[i].name[0]);\r
++      size_t minlen = 0;\r
++      char keyword[128];\r
++      char *p;\r
++\r
++      strncpy (keyword, _(keywords[i].name), sizeof (keyword));\r
++\r
++      /* Truncate too long keywords. REVISIT: Make this dynamic? */\r
++      keyword[sizeof (keyword) - 1] = '\0';\r
++\r
++      /* Minimum match length. */\r
++      p = strchr (keyword, '|');\r
++      if (p) {\r
++          minlen = p - keyword;\r
++          memmove (p, p + 1, strlen (p + 1) + 1);\r
++      }\r
++\r
++      n = stringcmp (s, keyword, minlen, mcase);\r
++      if (n > max_n || (n == max_n && mcase)) {\r
++          max_n = n;\r
++          kw = &keywords[i];\r
++      }\r
++    }\r
++\r
++    if (!kw)\r
++      return -PARSE_TIME_ERR_KEYWORD;\r
++\r
++    if (kw->set)\r
++      r = kw->set (state, kw);\r
++    else\r
++      r = kw_set_default (state, kw);\r
++\r
++    if (r < 0)\r
++      return r;\r
++\r
++    return 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 = hour * 60 + min;\r
++\r
++    assert (sign == '+' || sign == '-');\r
++\r
++    if (hour < 0 || hour > 14 || min < 0 || min > 59 || min % 15)\r
++      return -PARSE_TIME_ERR_INVALIDTIME;\r
++\r
++    if (sign == '-')\r
++      tz = -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, char d)\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
++    if (n == 1 || n == 2) {\r
++      if (state->last_field == TM_ABS_MON) {\r
++          /* D[D] */\r
++          if (!is_valid_mday (v))\r
++              return -PARSE_TIME_ERR_INVALIDDATE;\r
++\r
++          return set_field (state, TM_ABS_MDAY, v);\r
++      } else if (n == 2) {\r
++          /* REVISIT: only allow if last field is hour, min, or sec? */\r
++          if (d == '+' || d == '-') {\r
++              /* +/-HH */\r
++              return set_user_tz (state, d, v, 0);\r
++          }\r
++      }\r
++    } else if (n == 4) {\r
++      /* Notable exception: Value affects parsing. */\r
++      if (!is_valid_year (v)) {\r
++          if (d == '+' || d == '-') {\r
++              /* +/-HHMM */\r
++              return set_user_tz (state, d, v / 100, v % 100);\r
++          }\r
++      } else {\r
++          /* YYYY */\r
++          return set_field (state, TM_ABS_YEAR, v);\r
++      }\r
++    } else if (n == 6) {\r
++      /* HHMMSS */\r
++      int hour = v / 10000;\r
++      int min = (v / 100) % 100;\r
++      int sec = v % 100;\r
++\r
++      if (!is_valid_time (hour, min, sec))\r
++          return -PARSE_TIME_ERR_INVALIDTIME;\r
++\r
++      return set_abs_time (state, hour, min, sec);\r
++    }\r
++\r
++    /* else n is one of {-1, 3, 5, 7 } */\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 == 8) {\r
++      /* YYYYMMDD */\r
++      int year = v / 10000;\r
++      int mon = (v / 100) % 100;\r
++      int mday = v % 100;\r
++\r
++      if (!is_valid_date (year, mon, mday))\r
++          return -PARSE_TIME_ERR_INVALIDDATE;\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
++\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 == ':';\r
++}\r
++\r
++static bool\r
++is_date_sep (char c)\r
++{\r
++    return c == '/' || c == '-' || c == '.';\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 == 0 keep unset */\r
++static int\r
++expand_year (unsigned long year, size_t n)\r
++{\r
++    if (n == 2) {\r
++      return (year < 70 ? 2000 : 1900) + year;\r
++    } else if (n == 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 = UNSET, mon = UNSET, mday = 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 != 1 && n1 != 2)\r
++          return -PARSE_TIME_ERR_DATEFORMAT;\r
++\r
++      if ((n2 == 1 || n2 == 2) && (n3 == 0 || n3 == 2 || n3 == 4)) {\r
++          /* M[M]/D[D][/YY[YY]] */\r
++          year = expand_year (v3, n3);\r
++          mon = v1;\r
++          mday = v2;\r
++      } else if (n2 == 4 && n3 == 0) {\r
++          /* M[M]/YYYY */\r
++          year = v2;\r
++          mon = 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 == 4 && n2 == 2 && (n3 == 0 || n3 == 2)) {\r
++          /* YYYY-MM[-DD] */\r
++          year = v1;\r
++          mon = v2;\r
++          if (n3)\r
++              mday = v3;\r
++      } else if (n1 == 2 && n2 == 2 && (n3 == 0 || n3 == 2 || n3 == 4)) {\r
++          /* DD-MM[-YY[YY]] */\r
++          year = expand_year (v3, n3);\r
++          mon = v2;\r
++          mday = v1;\r
++      } else if (n1 == 2 && n2 == 4 && n3 == 0) {\r
++          /* MM-YYYY */\r
++          year = v2;\r
++          mon = 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 != 1 && n1 != 2) || (n2 != 1 && n2 != 2) ||\r
++          (n3 != 0 && n3 != 2 && n3 != 4))\r
++          return -PARSE_TIME_ERR_DATEFORMAT;\r
++\r
++      year = expand_year (v3, n3);\r
++      mon = v2;\r
++      mday = v1;\r
++      break;\r
++    }\r
++\r
++    if (year != UNSET && !is_valid_year (year))\r
++      return -PARSE_TIME_ERR_INVALIDDATE;\r
++\r
++    if (mon != UNSET && !is_valid_mon (mon))\r
++      return -PARSE_TIME_ERR_INVALIDDATE;\r
++\r
++    if (mday != UNSET && !is_valid_mday (mday))\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 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 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 == 2 && n2 == 2 && n3 == 0 &&\r
++      (state->delim == '+' || state->delim == '-')) {\r
++      return set_user_tz (state, state->delim, v1, v2);\r
++    }\r
++\r
++    if (!is_valid_time (v1, v2, v3))\r
++      return -PARSE_TIME_ERR_INVALIDTIME;\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 = strtoul (s, (char **) endp, 10);\r
++\r
++    *len = *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 = 0;\r
++    size_t n1, n2, n3 = 0;\r
++    const char *p = s;\r
++    char sep;\r
++\r
++    v1 = strtoul_len (p, &p, &n1);\r
++\r
++    if (is_sep (*p) && isdigit ((unsigned char) *(p + 1))) {\r
++      sep = *p;\r
++      v2 = strtoul_len (p + 1, &p, &n2);\r
++    } else {\r
++      /* a single number */\r
++      r = 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 == sep && isdigit ((unsigned char) *(p + 1)))\r
++      v3 = strtoul_len (p + 1, &p, &n3);\r
++\r
++    if (is_time_sep (sep))\r
++      r = parse_time (state, sep, v1, v2, v3, n1, n2, n3);\r
++    else\r
++      r = 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 = 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 = s;\r
++    ssize_t n;\r
++    int r;\r
++\r
++    while (*p) {\r
++      if (isalpha ((unsigned char) *p)) {\r
++          n = parse_keyword (state, p);\r
++      } else if (isdigit ((unsigned char) *p)) {\r
++          n = parse_number (state, p);\r
++      } else {\r
++          n = parse_delim (state, p);\r
++      }\r
++\r
++      if (n <= 0) {\r
++          if (n == 0)\r
++              n = -PARSE_TIME_ERR;\r
++\r
++          return n;\r
++      }\r
++\r
++      p += n;\r
++    }\r
++\r
++    /* parse postponed number, if any */\r
++    r = 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 = *now;\r
++    } else {\r
++      if (time (&t) == (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 += 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) == NULL)\r
++          return -PARSE_TIME_ERR_LIB;\r
++    } else {\r
++      /* local time */\r
++      if (localtime_r (&t, tm) == 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 = mktime (tm);\r
++\r
++    if (t == (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 = 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 = get_field (state, TM_ABS_HOUR);\r
++    if (!is_valid_12hour (hour))\r
++      return -PARSE_TIME_ERR_INVALIDTIME;\r
++\r
++    if (get_field (state, TM_AMPM)) {\r
++      /* 12pm is noon */\r
++      if (hour != 12)\r
++          hdiff = 12;\r
++    } else {\r
++      /* 12am is midnight, beginning of day */\r
++      if (hour == 12)\r
++          hdiff = -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 = { .tm_isdst = -1 };\r
++    struct tm now;\r
++    time_t t;\r
++    enum field f;\r
++    int r;\r
++    int week_round = PARSE_TIME_NO_ROUND;\r
++\r
++    r = initialize_now (state, &now, tnow);\r
++    if (r)\r
++      return r;\r
++\r
++    /* initialize uninitialized fields to now */\r
++    for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {\r
++      if (state->set[f] == FIELD_NOW) {\r
++          state->tm[f] = tm_get_field (&now, f);\r
++          state->set[f] = 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) >= tm_get_field (&now, TM_ABS_MON))\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 = get_field (state, TM_ABS_WDAY);\r
++      int today = tm_get_field (&now, TM_ABS_WDAY);\r
++      int rel_days;\r
++\r
++      if (today > wday)\r
++          rel_days = today - wday;\r
++      else\r
++          rel_days = 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 = fixup_ampm (state);\r
++    if (r)\r
++      return r;\r
++\r
++    /*\r
++     * Iterate fields from most accurate to least accurate, and set\r
++     * unset fields according to requested rounding.\r
++     */\r
++    for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {\r
++      if (round != PARSE_TIME_NO_ROUND) {\r
++          enum field r = abs_to_rel_field (f);\r
++\r
++          if (is_field_set (state, f) || is_field_set (state, r)) {\r
++              if (round >= PARSE_TIME_ROUND_UP)\r
++                  mod_field (state, r, -1);\r
++              round = PARSE_TIME_NO_ROUND; /* no more rounding */\r
++          } else {\r
++              if (f == TM_ABS_MDAY &&\r
++                  is_field_set (state, TM_REL_WEEK)) {\r
++                  /* week is most accurate */\r
++                  week_round = round;\r
++                  round = 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 != 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 = get_field (state, TM_ABS_SEC) - get_field (state, TM_REL_SEC);\r
++    tm.tm_min = get_field (state, TM_ABS_MIN) - get_field (state, TM_REL_MIN);\r
++    tm.tm_hour = get_field (state, TM_ABS_HOUR) - get_field (state, TM_REL_HOUR);\r
++    tm.tm_mday = get_field (state, TM_ABS_MDAY) -\r
++               get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK);\r
++    tm.tm_mon = get_field (state, TM_ABS_MON) - get_field (state, TM_REL_MON);\r
++    tm.tm_mon--; /* 1- to 0-based */\r
++    tm.tm_year = get_field (state, TM_ABS_YEAR) - get_field (state, 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 = -1;\r
++\r
++    /* special case: rounding with week accuracy */\r
++    if (week_round != PARSE_TIME_NO_ROUND) {\r
++      /* normalize to get proper tm.wday */\r
++      r = normalize_tm (&tm);\r
++      if (r < 0)\r
++          return r;\r
++\r
++      /* set more accurate fields back to zero */\r
++      tm.tm_sec = 0;\r
++      tm.tm_min = 0;\r
++      tm.tm_hour = 0;\r
++      tm.tm_isdst = -1;\r
++\r
++      /* monday is the true 1st day of week, but this is easier */\r
++      if (week_round <= PARSE_TIME_ROUND_DOWN)\r
++          tm.tm_mday -= tm.tm_wday;\r
++      else\r
++          tm.tm_mday += 7 - tm.tm_wday;\r
++    }\r
++\r
++    if (is_field_set (state, TM_TZ)) {\r
++      /* tm is in specified TZ, convert to UTC for timegm(3) */\r
++      tm.tm_min -= get_field (state, TM_TZ);\r
++      t = timegm (&tm);\r
++    } else {\r
++      /* tm is in local time */\r
++      t = mktime (&tm);\r
++    }\r
++\r
++    if (t == (time_t) -1)\r
++      return -PARSE_TIME_ERR_LIB;\r
++\r
++    *t_out = t;\r
++\r
++    return 0;\r
++}\r
++\r
++/* internally, all errors are < 0. parse_time_string() returns errors > 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 round)\r
++{\r
++    struct state state = { .last_field = TM_NONE };\r
++    int r;\r
++\r
++    if (!s || !t)\r
++      return EXTERNAL_ERR (-PARSE_TIME_ERR);\r
++\r
++    r = parse_input (&state, s);\r
++    if (r < 0)\r
++      return EXTERNAL_ERR (r);\r
++\r
++    r = 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 © 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
++ * 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 = 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 = -1,\r
++    PARSE_TIME_NO_ROUND = 0,\r
++    PARSE_TIME_ROUND_UP = 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, int round);\r
++\r
++#ifdef __cplusplus\r
++}\r
++#endif\r
++\r
++#endif /* PARSE_TIME_STRING_H */\r
+-- \r
+1.7.9.5\r
+\r