From 91be631c66cd944343f859edfceeb1298e7252b8 Mon Sep 17 00:00:00 2001 From: Jani Nikula Date: Tue, 30 Oct 2012 22:32:33 +0200 Subject: [PATCH] [PATCH v6 2/9] parse-time-string: add a date/time parser to notmuch --- 82/c51a50f653c7ad21135a8cbd092887662ddee3 | 1769 +++++++++++++++++++++ 1 file changed, 1769 insertions(+) create mode 100644 82/c51a50f653c7ad21135a8cbd092887662ddee3 diff --git a/82/c51a50f653c7ad21135a8cbd092887662ddee3 b/82/c51a50f653c7ad21135a8cbd092887662ddee3 new file mode 100644 index 000000000..ede456c9b --- /dev/null +++ b/82/c51a50f653c7ad21135a8cbd092887662ddee3 @@ -0,0 +1,1769 @@ +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 9C106431FAF + for ; Tue, 30 Oct 2012 13:33:23 -0700 (PDT) +X-Virus-Scanned: Debian amavisd-new at olra.theworths.org +X-Amavis-Alert: BAD HEADER SECTION, Duplicate header field: "References" +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 YDfHObjEMH0x for ; + Tue, 30 Oct 2012 13:33:11 -0700 (PDT) +Received: from mail-la0-f53.google.com (mail-la0-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 855D7431FDA + for ; Tue, 30 Oct 2012 13:32:53 -0700 (PDT) +Received: by mail-la0-f53.google.com with SMTP id l5so545085lah.26 + for ; Tue, 30 Oct 2012 13:32:53 -0700 (PDT) +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=google.com; s=20120113; + h=from:to:cc:subject:date:message-id:x-mailer:in-reply-to:references + :in-reply-to:references:mime-version:content-type + :content-transfer-encoding:x-gm-message-state; + bh=5NR+S+BeBnkpIhpjRuOxHo4ArzlxYoWYSCOfbt7GJJw=; + b=ncB8YvdPHs4dloJoQNyxScAGGKLHefU5sAvFjg3qqFa0b0gK9hcTw/ExZgf+HRg8D8 + 8xwtuAtxMFFT1HzSyVInXgkRzPQy02eiIsMgCPFF7gQslbA0/cK2kXy6BnovPvWqZ5pY + CRU4vu8D6W1d1Q7IASj0urN/U0QY6UjXOYnt1J1oJbFCtTnHuZXEaDJCGrYPrC1Mc4U8 + X+Z/OJEzpnuQTY4b4fsQjeuXp2vY6p5z9JvU/82xpG51nmSIzTTTOhA+JAhi0HdbS2SW + BpV1soSiPscfYJEUt1VMlzSroZJZSDhQADtZemUxLnEnMYtZzoP0SRymA/HUT7pr1VJY + AxdA== +Received: by 10.112.103.135 with SMTP id fw7mr13650666lbb.16.1351629172925; + Tue, 30 Oct 2012 13:32:52 -0700 (PDT) +Received: from localhost (dsl-hkibrasgw4-fe51df00-27.dhcp.inet.fi. + [80.223.81.27]) + by mx.google.com with ESMTPS id f2sm761306lbz.6.2012.10.30.13.32.50 + (version=SSLv3 cipher=OTHER); Tue, 30 Oct 2012 13:32:51 -0700 (PDT) +From: Jani Nikula +To: notmuch@notmuchmail.org +Subject: [PATCH v6 2/9] parse-time-string: add a date/time parser to notmuch +Date: Tue, 30 Oct 2012 22:32:33 +0200 +Message-Id: + <144fbacede6d76e4e5c1980ee86ed0e6e013f876.1351626272.git.jani@nikula.org> +X-Mailer: git-send-email 1.7.10.4 +In-Reply-To: +References: +In-Reply-To: +References: +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +X-Gm-Message-State: + ALoCoQkCpbUhMVjBK1WFv5RYyiQiymLxqvtG9l4L+fgEYqJajV3vXlkdfFXSGDD4IzIgn7wgZmEA +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: Tue, 30 Oct 2012 20:33:24 -0000 + +Add a date/time parser to notmuch, to be used for adding date range +query support for notmuch lib later on. Add the parser to a directory +of its own to make it independent of the rest of the notmuch code +base. + +Signed-off-by: Jani Nikula +--- + Makefile | 2 +- + parse-time-string/Makefile | 5 + + parse-time-string/Makefile.local | 12 + + parse-time-string/README | 9 + + parse-time-string/parse-time-string.c | 1503 +++++++++++++++++++++++++++++++++ + parse-time-string/parse-time-string.h | 102 +++ + 6 files changed, 1632 insertions(+), 1 deletion(-) + create mode 100644 parse-time-string/Makefile + create mode 100644 parse-time-string/Makefile.local + create mode 100644 parse-time-string/README + create mode 100644 parse-time-string/parse-time-string.c + create mode 100644 parse-time-string/parse-time-string.h + +diff --git a/Makefile b/Makefile +index e5e2e3a..bb9c316 100644 +--- a/Makefile ++++ b/Makefile +@@ -3,7 +3,7 @@ + all: + + # List all subdirectories here. Each contains its own Makefile.local +-subdirs = compat completion emacs lib man util test ++subdirs = compat completion emacs lib man parse-time-string util test + + # We make all targets depend on the Makefiles themselves. + global_deps = Makefile Makefile.config Makefile.local \ +diff --git a/parse-time-string/Makefile b/parse-time-string/Makefile +new file mode 100644 +index 0000000..fa25832 +--- /dev/null ++++ b/parse-time-string/Makefile +@@ -0,0 +1,5 @@ ++all: ++ $(MAKE) -C .. all ++ ++.DEFAULT: ++ $(MAKE) -C .. $@ +diff --git a/parse-time-string/Makefile.local b/parse-time-string/Makefile.local +new file mode 100644 +index 0000000..53534f3 +--- /dev/null ++++ b/parse-time-string/Makefile.local +@@ -0,0 +1,12 @@ ++dir := parse-time-string ++extra_cflags += -I$(srcdir)/$(dir) ++ ++libparse-time-string_c_srcs := $(dir)/parse-time-string.c ++ ++libparse-time-string_modules := $(libparse-time-string_c_srcs:.c=.o) ++ ++$(dir)/libparse-time-string.a: $(libparse-time-string_modules) ++ $(call quiet,AR) rcs $@ $^ ++ ++SRCS := $(SRCS) $(libparse-time-string_c_srcs) ++CLEAN := $(CLEAN) $(libparse-time-string_modules) $(dir)/libparse-time-string.a +diff --git a/parse-time-string/README b/parse-time-string/README +new file mode 100644 +index 0000000..300ff1f +--- /dev/null ++++ b/parse-time-string/README +@@ -0,0 +1,9 @@ ++PARSE TIME STRING ++================= ++ ++parse_time_string() is a date/time parser originally written for ++notmuch by Jani Nikula . However, there is nothing ++notmuch specific in it, and it should be kept reusable for other ++projects, and ready to be packaged on its own as needed. Please do not ++add dependencies on or references to anything notmuch specific. The ++parser should only depend on the C library. +diff --git a/parse-time-string/parse-time-string.c b/parse-time-string/parse-time-string.c +new file mode 100644 +index 0000000..584067d3 +--- /dev/null ++++ b/parse-time-string/parse-time-string.c +@@ -0,0 +1,1503 @@ ++/* ++ * parse time string - user friendly date and time parser ++ * Copyright © 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 ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "parse-time-string.h" ++ ++/* ++ * IMPLEMENTATION DETAILS ++ * ++ * At a high level, the parsing is done in two phases: 1) actual ++ * parsing of the input string and storing the parsed data into ++ * 'struct state', and 2) processing of the data in 'struct state' ++ * according to current time (or provided reference time) and ++ * rounding. This is evident in the main entry point function ++ * parse_time_string(). ++ * ++ * 1) The parsing phase - parse_input() ++ * ++ * Parsing is greedy and happens from left to right. The parsing is as ++ * unambiguous as possible; only unambiguous date/time formats are ++ * accepted. Redundant or contradictory absolute date/time in the ++ * input (e.g. date specified multiple times/ways) is not ++ * accepted. Relative date/time on the other hand just accumulates if ++ * present multiple times (e.g. "5 days 5 days" just turns into 10 ++ * days). ++ * ++ * Parsing decisions are made on the input format, not value. For ++ * example, "20/5/2005" fails because the recognized format here is ++ * MM/D/YYYY, even though the values would suggest DD/M/YYYY. ++ * ++ * Parsing is mostly stateless in the sense that parsing decisions are ++ * not made based on the values of previously parsed data, or whether ++ * certain data is present in the first place. (There are a few ++ * exceptions to the latter part, though, such as parsing of time zone ++ * that would otherwise look like plain time.) ++ * ++ * When the parser encounters a number that is not greedily parsed as ++ * part of a format, the interpretation is postponed until the next ++ * token is parsed. The parser for the next token may consume the ++ * previously postponed number. For example, when parsing "20 May" the ++ * meaning of "20" is not known until "May" is parsed. If the parser ++ * for the next token does not consume the postponed number, the ++ * number is handled as a "lone" number before parser for the next ++ * token finishes. ++ * ++ * 2) The processing phase - create_output() ++ * ++ * Once the parser in phase 1 has finished, 'struct state' contains ++ * all the information from the input string, and it's no longer ++ * needed. Since the parser does not even handle the concept of "now", ++ * the processing initializes the fields referring to the current ++ * date/time. ++ * ++ * If requested, the result is rounded towards past or future. The ++ * idea behind rounding is to support parsing date/time ranges in an ++ * obvious way. For example, for a range defined as two dates (without ++ * time), one would typically want to have an inclusive range from the ++ * beginning of start date to the end of the end date. The caller ++ * would use rounding towards past in the start date, and towards ++ * future in the end date. ++ * ++ * The absolute date and time is shifted by the relative date and ++ * time, and time zone adjustments are made. Daylight saving time ++ * (DST) is specifically *not* handled at all. ++ * ++ * Finally, the result is stored to time_t. ++ */ ++ ++#define unused(x) x __attribute__ ((unused)) ++ ++/* XXX: Redefine these to add i18n support. The keyword table uses ++ * N_() to mark strings to be translated; they are accessed ++ * dynamically using _(). */ ++#define _(s) (s) /* i18n: define as gettext (s) */ ++#define N_(s) (s) /* i18n: define as gettext_noop (s) */ ++ ++#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0])) ++ ++/* ++ * Field indices in the tm and set arrays of struct state. ++ * ++ * NOTE: There's some code that depends on the ordering of this enum. ++ */ ++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_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 absolute or reference time */ ++ 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 = TM_NONE, ++ TM_FIRST_ABS = TM_ABS_SEC, ++ TM_FIRST_REL = TM_REL_SEC, ++}; ++ ++/* Values for the set array of struct state. */ ++enum field_set { ++ FIELD_UNSET, /* The field has not been touched by parser. */ ++ FIELD_SET, /* The field has been set by parser. */ ++ FIELD_NOW, /* The field will be set to reference time. */ ++}; ++ ++static enum field ++next_abs_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 <= TM_ABS_YEAR); ++ ++ /* NOTE: Depends on the enum ordering. */ ++ return field + (TM_FIRST_REL - TM_FIRST_ABS); ++} ++ ++/* Get the smallest acceptable value for field. */ ++static int ++get_field_epoch_value (enum field field) ++{ ++ if (field == TM_ABS_MDAY || field == TM_ABS_MON) ++ return 1; ++ else if (field == TM_ABS_YEAR) ++ return 1970; ++ else ++ return 0; ++} ++ ++/* The parsing state. */ ++struct state { ++ int tm[TM_SIZE]; /* parsed date and time */ ++ enum field_set set[TM_SIZE]; /* set status of tm */ ++ ++ enum field last_field; /* Previously set field. */ ++ char delim; ++ ++ int postponed_length; /* Number of digits in postponed value. */ ++ int postponed_value; ++ char postponed_delim; /* The delimiter preceding postponed number. */ ++}; ++ ++/* ++ * 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; ++} ++ ++/* ++ * Consume a previously postponed number. Return true if a number was ++ * in fact postponed, false otherwise. Store the postponed number's ++ * value in *v, length in the input string in *n (or -1 if the number ++ * was written out and parsed as a keyword), and the preceding ++ * delimiter to *d. If a number was not postponed, *v, *n and *d are ++ * unchanged. ++ */ ++static bool ++consume_postponed_number (struct state *state, int *v, int *n, char *d) ++{ ++ if (!state->postponed_length) ++ return false; ++ ++ if (n) ++ *n = state->postponed_length; ++ ++ if (v) ++ *v = state->postponed_value; ++ ++ if (d) ++ *d = state->postponed_delim; ++ ++ state->postponed_length = 0; ++ state->postponed_value = 0; ++ state->postponed_delim = 0; ++ ++ return true; ++} ++ ++static int parse_postponed_number (struct state *state, enum field next_field); ++ ++/* ++ * Postpone a 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; ++ char d = state->delim; ++ ++ /* Parse a previously postponed number, if any. */ ++ r = parse_postponed_number (state, TM_NONE); ++ if (r) ++ return r; ++ ++ state->postponed_length = n; ++ state->postponed_value = v; ++ state->postponed_delim = d; ++ ++ return 0; ++} ++ ++static void ++set_delim (struct state *state, char delim) ++{ ++ state->delim = delim; ++} ++ ++static void ++unset_delim (struct state *state) ++{ ++ state->delim = 0; ++} ++ ++/* ++ * Field set/get/mod helpers. ++ */ ++ ++/* Return true if field has been set. */ ++static bool ++is_field_set (struct state *state, enum field field) ++{ ++ assert (field < ARRAY_SIZE (state->tm)); ++ ++ return state->set[field] != FIELD_UNSET; ++} ++ ++static void ++unset_field (struct state *state, enum field field) ++{ ++ assert (field < ARRAY_SIZE (state->tm)); ++ ++ state->set[field] = FIELD_UNSET; ++ state->tm[field] = 0; ++} ++ ++/* ++ * Set field to value. A field can only be set once to ensure the ++ * input does not contain redundant and potentially conflicting data. ++ */ ++static int ++set_field (struct state *state, enum field field, int value) ++{ ++ int r; ++ ++ /* Fields can only be set once. */ ++ if (is_field_set (state, field)) ++ return -PARSE_TIME_ERR_ALREADYSET; ++ ++ state->set[field] = FIELD_SET; ++ ++ /* Parse a previously postponed number, if any. */ ++ r = parse_postponed_number (state, field); ++ if (r) ++ return r; ++ ++ unset_delim (state); ++ ++ state->tm[field] = value; ++ state->last_field = field; ++ ++ return 0; ++} ++ ++/* ++ * Mark n fields in fields to be set to the reference 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 = 0; i < n; i++) { ++ r = set_field (state, fields[i], 0); ++ if (r) ++ return r; ++ state->set[fields[i]] = FIELD_NOW; ++ } ++ ++ return 0; ++} ++ ++/* Modify field by adding value to it. To be used on relative fields, ++ * which can be modified multiple times (to accumulate). */ ++static int ++add_to_field (struct state *state, enum field field, int value) ++{ ++ int r; ++ ++ assert (field < ARRAY_SIZE (state->tm)); ++ ++ state->set[field] = FIELD_SET; ++ ++ /* Parse a previously postponed number, if any. */ ++ r = parse_postponed_number (state, field); ++ if (r) ++ return r; ++ ++ unset_delim (state); ++ ++ state->tm[field] += value; ++ state->last_field = 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]; ++} ++ ++/* ++ * Validity checkers. ++ */ ++static bool is_valid_12hour (int h) ++{ ++ return h >= 1 && h <= 12; ++} ++ ++static bool is_valid_time (int h, int m, int s) ++{ ++ /* Allow 24:00:00 to denote end of day. */ ++ if (h == 24 && m == 0 && s == 0) ++ return true; ++ ++ return h >= 0 && h <= 23 && m >= 0 && m <= 59 && s >= 0 && s <= 59; ++} ++ ++static bool is_valid_mday (int mday) ++{ ++ return mday >= 1 && mday <= 31; ++} ++ ++static bool is_valid_mon (int mon) ++{ ++ return mon >= 1 && mon <= 12; ++} ++ ++static bool is_valid_year (int year) ++{ ++ return year >= 1970; ++} ++ ++static bool is_valid_date (int year, int mon, int mday) ++{ ++ return is_valid_year (year) && is_valid_mon (mon) && is_valid_mday (mday); ++} ++ ++/* 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 != UNSET) { ++ if ((r = set_field (state, TM_ABS_HOUR, hour))) ++ return r; ++ } ++ ++ if (min != UNSET) { ++ if ((r = set_field (state, TM_ABS_MIN, min))) ++ return r; ++ } ++ ++ if (sec != UNSET) { ++ if ((r = 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 != UNSET) { ++ if ((r = set_field (state, TM_ABS_YEAR, year))) ++ return r; ++ } ++ ++ if (mon != UNSET) { ++ if ((r = set_field (state, TM_ABS_MON, mon))) ++ return r; ++ } ++ ++ if (mday != UNSET) { ++ if ((r = 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 */ ++ 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_rel (struct state *state, struct keyword *kw) ++{ ++ int multiplier = 1; ++ ++ /* Get a previously set multiplier, if any. */ ++ consume_postponed_number (state, &multiplier, NULL, NULL); ++ ++ /* Accumulate relative field values. */ ++ return add_to_field (state, kw->field, multiplier * kw->value); ++} ++ ++static int ++kw_set_number (struct state *state, struct keyword *kw) ++{ ++ /* -1 = 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 = get_postponed_length (state); ++ ++ /* Consume postponed number if it could be mday. This handles "20 ++ * January". */ ++ if (n == 1 || n == 2) { ++ int r, v; ++ ++ consume_postponed_number (state, &v, NULL, NULL); ++ ++ if (!is_valid_mday (v)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ r = 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 = get_postponed_length (state); ++ ++ /* Consume postponed number if it could be hour. This handles ++ * "5pm". */ ++ if (n == 1 || n == 2) { ++ int r, v; ++ ++ consume_postponed_number (state, &v, NULL, NULL); ++ ++ if (!is_valid_12hour (v)) ++ return -PARSE_TIME_ERR_INVALIDTIME; ++ ++ r = 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, unused (struct keyword *kw)) ++{ ++ enum field fields[] = { 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, unused (struct keyword *kw)) ++{ ++ enum field fields[] = { 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 (!consume_postponed_number (state, &v, &n, NULL)) ++ return -PARSE_TIME_ERR_DATEFORMAT; ++ ++ /* Ordinals are mday. */ ++ if (n != 1 && n != 2) ++ return -PARSE_TIME_ERR_DATEFORMAT; ++ ++ /* Be strict about st, nd, rd, and lax about th. */ ++ if (strcasecmp (kw->name, "st") == 0 && v != 1 && v != 21 && v != 31) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ else if (strcasecmp (kw->name, "nd") == 0 && v != 2 && v != 22) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ else if (strcasecmp (kw->name, "rd") == 0 && v != 3 && v != 23) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ else if (strcasecmp (kw->name, "th") == 0 && !is_valid_mday (v)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ return set_field (state, TM_ABS_MDAY, v); ++} ++ ++static int ++kw_ignore (unused (struct state *state), unused (struct keyword *kw)) ++{ ++ return 0; ++} ++ ++/* ++ * Accepted keywords. ++ * ++ * A keyword may optionally contain a '|' to indicate the minimum ++ * match length. Without one, full match is required. It's advisable ++ * to keep the minimum match parts unique across all keywords. If ++ * they're not, the first match wins. ++ * ++ * If keyword begins with '*', then the matching will be case ++ * sensitive. Otherwise the matching is case insensitive. ++ * ++ * If .set is NULL, the field specified by .field will be set to ++ * .value. ++ * ++ * Note: Observe how "m" and "mi" match minutes, "M" and "mo" and ++ * "mont" match months, but "mon" matches Monday. ++ */ ++static struct keyword keywords[] = { ++ /* Weekdays. */ ++ { N_("sun|day"), TM_WDAY, 0, NULL }, ++ { N_("mon|day"), TM_WDAY, 1, NULL }, ++ { N_("tue|sday"), TM_WDAY, 2, NULL }, ++ { N_("wed|nesday"), TM_WDAY, 3, NULL }, ++ { N_("thu|rsday"), TM_WDAY, 4, NULL }, ++ { N_("fri|day"), TM_WDAY, 5, NULL }, ++ { N_("sat|urday"), TM_WDAY, 6, NULL }, ++ ++ /* Months. */ ++ { N_("jan|uary"), TM_ABS_MON, 1, kw_set_month }, ++ { N_("feb|ruary"), TM_ABS_MON, 2, kw_set_month }, ++ { N_("mar|ch"), TM_ABS_MON, 3, kw_set_month }, ++ { N_("apr|il"), TM_ABS_MON, 4, kw_set_month }, ++ { N_("may"), TM_ABS_MON, 5, kw_set_month }, ++ { N_("jun|e"), TM_ABS_MON, 6, kw_set_month }, ++ { N_("jul|y"), TM_ABS_MON, 7, kw_set_month }, ++ { N_("aug|ust"), TM_ABS_MON, 8, kw_set_month }, ++ { N_("sep|tember"), TM_ABS_MON, 9, kw_set_month }, ++ { N_("oct|ober"), TM_ABS_MON, 10, kw_set_month }, ++ { N_("nov|ember"), TM_ABS_MON, 11, kw_set_month }, ++ { N_("dec|ember"), TM_ABS_MON, 12, kw_set_month }, ++ ++ /* Durations. */ ++ { N_("y|ears"), TM_REL_YEAR, 1, kw_set_rel }, ++ { N_("mo|nths"), TM_REL_MON, 1, kw_set_rel }, ++ { N_("*M"), TM_REL_MON, 1, kw_set_rel }, ++ { N_("w|eeks"), TM_REL_WEEK, 1, kw_set_rel }, ++ { N_("d|ays"), TM_REL_DAY, 1, kw_set_rel }, ++ { N_("h|ours"), TM_REL_HOUR, 1, kw_set_rel }, ++ { N_("hr|s"), TM_REL_HOUR, 1, kw_set_rel }, ++ { N_("mi|nutes"), TM_REL_MIN, 1, kw_set_rel }, ++ { N_("mins"), TM_REL_MIN, 1, kw_set_rel }, ++ { N_("*m"), TM_REL_MIN, 1, kw_set_rel }, ++ { N_("s|econds"), TM_REL_SEC, 1, kw_set_rel }, ++ { N_("secs"), TM_REL_SEC, 1, kw_set_rel }, ++ ++ /* Numbers. */ ++ { N_("one"), TM_NONE, 1, kw_set_number }, ++ { N_("two"), TM_NONE, 2, kw_set_number }, ++ { N_("three"), TM_NONE, 3, kw_set_number }, ++ { N_("four"), TM_NONE, 4, kw_set_number }, ++ { N_("five"), TM_NONE, 5, kw_set_number }, ++ { N_("six"), TM_NONE, 6, kw_set_number }, ++ { N_("seven"), TM_NONE, 7, kw_set_number }, ++ { N_("eight"), TM_NONE, 8, kw_set_number }, ++ { N_("nine"), TM_NONE, 9, kw_set_number }, ++ { N_("ten"), TM_NONE, 10, kw_set_number }, ++ { N_("dozen"), TM_NONE, 12, kw_set_number }, ++ { N_("hundred"), TM_NONE, 100, kw_set_number }, ++ ++ /* Special number forms. */ ++ { N_("this"), TM_NONE, 0, kw_set_number }, ++ { N_("last"), TM_NONE, 1, kw_set_number }, ++ ++ /* Other special keywords. */ ++ { N_("yesterday"), TM_REL_DAY, 1, kw_set_rel }, ++ { N_("today"), TM_NONE, 0, kw_set_today }, ++ { N_("now"), TM_NONE, 0, kw_set_now }, ++ { N_("noon"), TM_NONE, 12, kw_set_timeofday }, ++ { N_("midnight"), TM_NONE, 0, kw_set_timeofday }, ++ { N_("am"), TM_AMPM, 0, kw_set_ampm }, ++ { N_("a.m."), TM_AMPM, 0, kw_set_ampm }, ++ { N_("pm"), TM_AMPM, 1, kw_set_ampm }, ++ { N_("p.m."), TM_AMPM, 1, kw_set_ampm }, ++ { N_("st"), TM_NONE, 0, kw_set_ordinal }, ++ { N_("nd"), TM_NONE, 0, kw_set_ordinal }, ++ { N_("rd"), TM_NONE, 0, kw_set_ordinal }, ++ { N_("th"), TM_NONE, 0, kw_set_ordinal }, ++ { N_("ago"), TM_NONE, 0, kw_ignore }, ++ ++ /* Timezone codes: offset in minutes. XXX: Add more codes. */ ++ { N_("pst"), TM_TZ, -8*60, NULL }, ++ { N_("mst"), TM_TZ, -7*60, NULL }, ++ { N_("cst"), TM_TZ, -6*60, NULL }, ++ { N_("est"), TM_TZ, -5*60, NULL }, ++ { N_("ast"), TM_TZ, -4*60, NULL }, ++ { N_("nst"), TM_TZ, -(3*60+30), NULL }, ++ ++ { N_("gmt"), TM_TZ, 0, NULL }, ++ { N_("utc"), TM_TZ, 0, NULL }, ++ ++ { N_("wet"), TM_TZ, 0, NULL }, ++ { N_("cet"), TM_TZ, 1*60, NULL }, ++ { N_("eet"), TM_TZ, 2*60, NULL }, ++ { N_("fet"), TM_TZ, 3*60, NULL }, ++ ++ { N_("wat"), TM_TZ, 1*60, NULL }, ++ { N_("cat"), TM_TZ, 2*60, NULL }, ++ { N_("eat"), TM_TZ, 3*60, NULL }, ++}; ++ ++/* ++ * Compare strings str and keyword. Return the number of matching ++ * chars on match, 0 for no match. ++ * ++ * All of the alphabetic characters (isalpha) in str up to the first ++ * non-alpha character (or end of string) must match the ++ * keyword. Consequently, the value returned on match is the number of ++ * consecutive alphabetic characters in str. ++ * ++ * Abbreviated match is accepted if the keyword contains a '|' ++ * character, and str matches keyword up to that character. Any alpha ++ * characters after that in str must still match the keyword following ++ * the '|' character. If no '|' is present, all of keyword must match. ++ * ++ * Excessive, consecutive, and misplaced (at the beginning or end) '|' ++ * characters in keyword are handled gracefully. Only the first one ++ * matters. ++ * ++ * If match_case is true, the matching is case sensitive. ++ */ ++static size_t ++match_keyword (const char *str, const char *keyword, bool match_case) ++{ ++ const char *s = str; ++ bool prefix_matched = false; ++ ++ for (;;) { ++ while (*keyword == '|') { ++ prefix_matched = true; ++ keyword++; ++ } ++ ++ if (!*s || !isalpha ((unsigned char) *s) || !*keyword) ++ break; ++ ++ if (match_case) { ++ if (*s != *keyword) ++ return 0; ++ } else { ++ if (tolower ((unsigned char) *s) != ++ tolower ((unsigned char) *keyword)) ++ return 0; ++ } ++ s++; ++ keyword++; ++ } ++ ++ /* did not match all of the keyword in input string */ ++ if (*s && isalpha ((unsigned char) *s)) ++ return 0; ++ ++ /* did not match enough of keyword */ ++ if (*keyword && !prefix_matched) ++ return 0; ++ ++ return s - str; ++} ++ ++/* ++ * 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 = 0; ++ struct keyword *kw = NULL; ++ int r; ++ ++ for (i = 0; i < ARRAY_SIZE (keywords); i++) { ++ const char *keyword = _(keywords[i].name); ++ bool mcase = false; ++ ++ /* Match case if keyword begins with '*'. */ ++ if (*keyword == '*') { ++ mcase = true; ++ keyword++; ++ } ++ ++ n = match_keyword (s, keyword, mcase); ++ if (n) { ++ kw = &keywords[i]; ++ break; ++ } ++ } ++ ++ if (!kw) ++ return -PARSE_TIME_ERR_KEYWORD; ++ ++ if (kw->set) ++ r = kw->set (state, kw); ++ else ++ r = set_field (state, kw->field, kw->value); ++ ++ if (r < 0) ++ return r; ++ ++ return n; ++} ++ ++/* ++ * Non-keyword parsers and their helpers. ++ */ ++ ++static int ++set_user_tz (struct state *state, char sign, int hour, int min) ++{ ++ int tz = hour * 60 + min; ++ ++ assert (sign == '+' || sign == '-'); ++ ++ if (hour < 0 || hour > 14 || min < 0 || min > 59 || min % 15) ++ return -PARSE_TIME_ERR_INVALIDTIME; ++ ++ if (sign == '-') ++ tz = -tz; ++ ++ return set_field (state, TM_TZ, tz); ++} ++ ++/* ++ * Parse a previously postponed number if one exists. Independent ++ * parsing of a postponed number when it wasn't consumed during ++ * parsing of the following token. ++ */ ++static int ++parse_postponed_number (struct state *state, unused (enum field next_field)) ++{ ++ int v, n; ++ char d; ++ ++ /* Bail out if there's no postponed number. */ ++ if (!consume_postponed_number (state, &v, &n, &d)) ++ return 0; ++ ++ if (n == 1 || n == 2) { ++ /* Notable exception: Previous field affects parsing. This ++ * handles "January 20". */ ++ if (state->last_field == TM_ABS_MON) { ++ /* D[D] */ ++ if (!is_valid_mday (v)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ return set_field (state, TM_ABS_MDAY, v); ++ } else if (n == 2) { ++ /* XXX: Only allow if last field is hour, min, or sec? */ ++ if (d == '+' || d == '-') { ++ /* +/-HH */ ++ return set_user_tz (state, d, v, 0); ++ } ++ } ++ } else if (n == 4) { ++ /* Notable exception: Value affects parsing. Time zones are ++ * always at most 1400 and we don't understand years before ++ * 1970. */ ++ if (!is_valid_year (v)) { ++ if (d == '+' || d == '-') { ++ /* +/-HHMM */ ++ return set_user_tz (state, d, v / 100, v % 100); ++ } ++ } else { ++ /* YYYY */ ++ return set_field (state, TM_ABS_YEAR, v); ++ } ++ } else if (n == 6) { ++ /* HHMMSS */ ++ int hour = v / 10000; ++ int min = (v / 100) % 100; ++ int sec = v % 100; ++ ++ if (!is_valid_time (hour, min, sec)) ++ return -PARSE_TIME_ERR_INVALIDTIME; ++ ++ return set_abs_time (state, hour, min, sec); ++ } else if (n == 8) { ++ /* YYYYMMDD */ ++ int year = v / 10000; ++ int mon = (v / 100) % 100; ++ int mday = v % 100; ++ ++ if (!is_valid_date (year, mon, mday)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ return set_abs_date (state, year, mon, mday); ++ } ++ ++ return -PARSE_TIME_ERR_FORMAT; ++} ++ ++static int tm_get_field (const struct tm *tm, enum field field); ++ ++static int ++set_timestamp (struct state *state, time_t t) ++{ ++ struct tm tm; ++ enum field f; ++ int r; ++ ++ if (gmtime_r (&t, &tm) == NULL) ++ return -PARSE_TIME_ERR_LIB; ++ ++ for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) { ++ r = set_field (state, f, tm_get_field (&tm, f)); ++ if (r) ++ return r; ++ } ++ ++ r = set_field (state, TM_TZ, 0); ++ if (r) ++ return r; ++ ++ /* XXX: Prevent TM_AMPM with timestamp, e.g. "@123456 pm" */ ++ ++ return 0; ++} ++ ++/* 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); ++ ++ if (state->delim == '@') ++ return set_timestamp (state, (time_t) v); ++ ++ 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 == ':'; ++} ++ ++static bool ++is_date_sep (char c) ++{ ++ return c == '/' || c == '-' || c == '.'; ++} ++ ++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 == 0 keep ++ * unset. */ ++static int ++expand_year (unsigned long year, size_t n) ++{ ++ if (n == 2) { ++ return (year < 70 ? 2000 : 1900) + year; ++ } else if (n == 4) { ++ return year; ++ } else { ++ return UNSET; ++ } ++} ++ ++/* Parse a date number triplet. */ ++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 = UNSET, mon = UNSET, mday = UNSET; ++ ++ assert (is_date_sep (sep)); ++ ++ switch (sep) { ++ case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */ ++ if (n1 != 1 && n1 != 2) ++ return -PARSE_TIME_ERR_DATEFORMAT; ++ ++ if ((n2 == 1 || n2 == 2) && (n3 == 0 || n3 == 2 || n3 == 4)) { ++ /* M[M]/D[D][/YY[YY]] */ ++ year = expand_year (v3, n3); ++ mon = v1; ++ mday = v2; ++ } else if (n2 == 4 && n3 == 0) { ++ /* M[M]/YYYY */ ++ year = v2; ++ mon = v1; ++ } else { ++ return -PARSE_TIME_ERR_DATEFORMAT; ++ } ++ break; ++ ++ case '-': /* Date: YYYY-MM[-DD] or DD-MM[-YY[YY]] or MM-YYYY */ ++ if (n1 == 4 && n2 == 2 && (n3 == 0 || n3 == 2)) { ++ /* YYYY-MM[-DD] */ ++ year = v1; ++ mon = v2; ++ if (n3) ++ mday = v3; ++ } else if (n1 == 2 && n2 == 2 && (n3 == 0 || n3 == 2 || n3 == 4)) { ++ /* DD-MM[-YY[YY]] */ ++ year = expand_year (v3, n3); ++ mon = v2; ++ mday = v1; ++ } else if (n1 == 2 && n2 == 4 && n3 == 0) { ++ /* MM-YYYY */ ++ year = v2; ++ mon = v1; ++ } else { ++ return -PARSE_TIME_ERR_DATEFORMAT; ++ } ++ break; ++ ++ case '.': /* Date: D[D].M[M][.[YY[YY]]] */ ++ if ((n1 != 1 && n1 != 2) || (n2 != 1 && n2 != 2) || ++ (n3 != 0 && n3 != 2 && n3 != 4)) ++ return -PARSE_TIME_ERR_DATEFORMAT; ++ ++ year = expand_year (v3, n3); ++ mon = v2; ++ mday = v1; ++ break; ++ } ++ ++ if (year != UNSET && !is_valid_year (year)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ if (mon != UNSET && !is_valid_mon (mon)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ if (mday != UNSET && !is_valid_mday (mday)) ++ return -PARSE_TIME_ERR_INVALIDDATE; ++ ++ return set_abs_date (state, year, mon, mday); ++} ++ ++/* Parse a time number triplet. */ ++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 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 2)) ++ return -PARSE_TIME_ERR_TIMEFORMAT; ++ ++ /* ++ * Notable exception: Previously set fields affect ++ * parsing. Interpret (+|-)HH:MM as time zone only if hour and ++ * minute have been set. ++ * ++ * XXX: This could be fixed by restricting the delimiters ++ * preceding time. For '+' it would be justified, but for '-' it ++ * might be inconvenient. However prefer to allow '-' as an ++ * insignificant delimiter preceding time for convenience, and ++ * handle '+' the same way for consistency between positive and ++ * negative time zones. ++ */ ++ if (is_field_set (state, TM_ABS_HOUR) && ++ is_field_set (state, TM_ABS_MIN) && ++ n1 == 2 && n2 == 2 && n3 == 0 && ++ (state->delim == '+' || state->delim == '-')) { ++ return set_user_tz (state, state->delim, v1, v2); ++ } ++ ++ if (!is_valid_time (v1, v2, v3)) ++ return -PARSE_TIME_ERR_INVALIDTIME; ++ ++ 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 = strtoul (s, (char **) endp, 10); ++ ++ *len = *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 = 0; ++ size_t n1, n2, n3 = 0; ++ const char *p = s; ++ char sep; ++ ++ v1 = strtoul_len (p, &p, &n1); ++ ++ if (!is_sep (*p) || !isdigit ((unsigned char) *(p + 1))) { ++ /* A single number. */ ++ r = parse_single_number (state, v1, n1); ++ if (r) ++ return r; ++ ++ return p - s; ++ } ++ ++ sep = *p; ++ v2 = strtoul_len (p + 1, &p, &n2); ++ ++ /* A group of two or three numbers? */ ++ if (*p == sep && isdigit ((unsigned char) *(p + 1))) ++ v3 = strtoul_len (p + 1, &p, &n3); ++ ++ if (is_time_sep (sep)) ++ r = parse_time (state, sep, v1, v2, v3, n1, n2, n3); ++ else ++ r = parse_date (state, sep, v1, v2, v3, n1, n2, n3); ++ ++ if (r) ++ return r; ++ ++ return p - s; ++} ++ ++/* ++ * Parse delimiter(s). Throw away all except the last one, which is ++ * stored for parsing the next non-delimiter. Return < 0 on error, ++ * number of parsed chars on success. ++ * ++ * XXX: We might want to be more strict here. ++ */ ++static ssize_t ++parse_delim (struct state *state, const char *s) ++{ ++ const char *p = s; ++ ++ /* ++ * 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 = s; ++ ssize_t n; ++ int r; ++ ++ while (*p) { ++ if (isalpha ((unsigned char) *p)) { ++ n = parse_keyword (state, p); ++ } else if (isdigit ((unsigned char) *p)) { ++ n = parse_number (state, p); ++ } else { ++ n = parse_delim (state, p); ++ } ++ ++ if (n <= 0) { ++ if (n == 0) ++ n = -PARSE_TIME_ERR; ++ ++ return n; ++ } ++ ++ p += n; ++ } ++ ++ /* Parse a previously postponed number, if any. */ ++ r = parse_postponed_number (state, TM_NONE); ++ 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, const time_t *ref, struct tm *tm) ++{ ++ time_t t; ++ ++ if (ref) { ++ t = *ref; ++ } else { ++ if (time (&t) == (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 += get_field (state, TM_TZ) * 60; ++ ++ /* It's not gm, but this doesn't mess with the TZ. */ ++ if (gmtime_r (&t, tm) == NULL) ++ return -PARSE_TIME_ERR_LIB; ++ } else { ++ /* Local time. */ ++ if (localtime_r (&t, tm) == NULL) ++ return -PARSE_TIME_ERR_LIB; ++ } ++ ++ return 0; ++} ++ ++/* ++ * Normalize tm according to mktime(3); if structure members are ++ * outside their valid interval, they will be normalized (so that, for ++ * example, 40 October is changed into 9 November), and tm_wday and ++ * tm_yday are set to values determined from the contents of the other ++ * fields. ++ * ++ * 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 = mktime (tm); ++ ++ if (t == (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_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 = 0; ++ ++ if (!is_field_set (state, TM_AMPM)) ++ return 0; ++ ++ if (!is_field_set (state, TM_ABS_HOUR)) ++ return -PARSE_TIME_ERR_TIMEFORMAT; ++ ++ hour = get_field (state, TM_ABS_HOUR); ++ if (!is_valid_12hour (hour)) ++ return -PARSE_TIME_ERR_INVALIDTIME; ++ ++ if (get_field (state, TM_AMPM)) { ++ /* 12pm is noon. */ ++ if (hour != 12) ++ hdiff = 12; ++ } else { ++ /* 12am is midnight, beginning of day. */ ++ if (hour == 12) ++ hdiff = -12; ++ } ++ ++ add_to_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 *ref, ++ int round) ++{ ++ struct tm tm = { .tm_isdst = -1 }; ++ struct tm now; ++ time_t t; ++ enum field f; ++ int r; ++ int week_round = PARSE_TIME_NO_ROUND; ++ ++ r = initialize_now (state, ref, &now); ++ if (r) ++ return r; ++ ++ /* Initialize fields flagged as "now" to reference time. */ ++ for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) { ++ if (state->set[f] == FIELD_NOW) { ++ state->tm[f] = tm_get_field (&now, f); ++ state->set[f] = FIELD_SET; ++ } ++ } ++ ++ /* ++ * If WDAY is set but MDAY is not, we consider WDAY relative ++ * ++ * XXX: This fails on stuff like "two months monday" because two ++ * months ago wasn't the same day as today. Postpone until we know ++ * date? ++ */ ++ if (is_field_set (state, TM_WDAY) && ++ !is_field_set (state, TM_ABS_MDAY)) { ++ int wday = get_field (state, TM_WDAY); ++ int today = tm_get_field (&now, TM_WDAY); ++ int rel_days; ++ ++ if (today > wday) ++ rel_days = today - wday; ++ else ++ rel_days = today + 7 - wday; ++ ++ /* This also prevents special week rounding from happening. */ ++ add_to_field (state, TM_REL_DAY, rel_days); ++ ++ unset_field (state, TM_WDAY); ++ } ++ ++ r = fixup_ampm (state); ++ if (r) ++ return r; ++ ++ /* ++ * Iterate fields from most accurate to least accurate, and set ++ * unset fields according to requested rounding. ++ */ ++ for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) { ++ if (round != PARSE_TIME_NO_ROUND) { ++ enum field r = abs_to_rel_field (f); ++ ++ if (is_field_set (state, f) || is_field_set (state, r)) { ++ if (round >= PARSE_TIME_ROUND_UP && f != TM_ABS_SEC) { ++ /* ++ * This is the most accurate field ++ * specified. Round up adjusting it towards ++ * future. ++ */ ++ add_to_field (state, r, -1); ++ ++ /* ++ * Go back a second if the result is to be used ++ * for inclusive comparisons. ++ */ ++ if (round == PARSE_TIME_ROUND_UP_INCLUSIVE) ++ add_to_field (state, TM_REL_SEC, 1); ++ } ++ round = PARSE_TIME_NO_ROUND; /* No more rounding. */ ++ } else { ++ if (f == TM_ABS_MDAY && ++ is_field_set (state, TM_REL_WEEK)) { ++ /* Week is most accurate. */ ++ week_round = round; ++ round = PARSE_TIME_NO_ROUND; ++ } else { ++ set_field (state, f, get_field_epoch_value (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 != 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 = get_field (state, TM_ABS_SEC) - get_field (state, TM_REL_SEC); ++ tm.tm_min = get_field (state, TM_ABS_MIN) - get_field (state, TM_REL_MIN); ++ tm.tm_hour = get_field (state, TM_ABS_HOUR) - get_field (state, TM_REL_HOUR); ++ tm.tm_mday = get_field (state, TM_ABS_MDAY) - ++ get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK); ++ tm.tm_mon = get_field (state, TM_ABS_MON) - get_field (state, TM_REL_MON); ++ tm.tm_mon--; /* 1- to 0-based */ ++ tm.tm_year = get_field (state, TM_ABS_YEAR) - get_field (state, TM_REL_YEAR) - 1900; ++ ++ /* ++ * It's always normal time. ++ * ++ * XXX: 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 = -1; ++ ++ /* Special case: rounding with week accuracy. */ ++ if (week_round != PARSE_TIME_NO_ROUND) { ++ /* Normalize to get proper tm.wday. */ ++ r = normalize_tm (&tm); ++ if (r < 0) ++ return r; ++ ++ /* Set more accurate fields back to zero. */ ++ tm.tm_sec = 0; ++ tm.tm_min = 0; ++ tm.tm_hour = 0; ++ tm.tm_isdst = -1; ++ ++ /* Monday is the true 1st day of week, but this is easier. */ ++ if (week_round >= PARSE_TIME_ROUND_UP) { ++ tm.tm_mday += 7 - tm.tm_wday; ++ if (week_round == PARSE_TIME_ROUND_UP_INCLUSIVE) ++ tm.tm_sec--; ++ } else { ++ tm.tm_mday -= tm.tm_wday; ++ } ++ } ++ ++ if (is_field_set (state, TM_TZ)) { ++ /* tm is in specified TZ, convert to UTC for timegm(3). */ ++ tm.tm_min -= get_field (state, TM_TZ); ++ t = timegm (&tm); ++ } else { ++ /* tm is in local time. */ ++ t = mktime (&tm); ++ } ++ ++ if (t == (time_t) -1) ++ return -PARSE_TIME_ERR_LIB; ++ ++ *t_out = t; ++ ++ 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 *ref, int round) ++{ ++ struct state state = { .last_field = TM_NONE }; ++ int r; ++ ++ if (!s || !t) ++ return EXTERNAL_ERR (-PARSE_TIME_ERR); ++ ++ r = parse_input (&state, s); ++ if (r < 0) ++ return EXTERNAL_ERR (r); ++ ++ r = create_output (&state, t, ref, round); ++ if (r < 0) ++ return EXTERNAL_ERR (r); ++ ++ return 0; ++} +diff --git a/parse-time-string/parse-time-string.h b/parse-time-string/parse-time-string.h +new file mode 100644 +index 0000000..bfa4ee3 +--- /dev/null ++++ b/parse-time-string/parse-time-string.h +@@ -0,0 +1,102 @@ ++/* ++ * parse time string - user friendly date and time parser ++ * Copyright © 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 = 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 = -1, ++ PARSE_TIME_NO_ROUND = 0, ++ PARSE_TIME_ROUND_UP = 1, ++ PARSE_TIME_ROUND_UP_INCLUSIVE = 2, ++}; ++ ++/** ++ * parse_time_string() - user friendly date and time parser ++ * @s: string to parse ++ * @t: pointer to time_t to store parsed time in ++ * @ref: 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 ++ * 'ref' 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), unless the most ++ * accurate unit is seconds. Rest of the unspecified units are set ++ * as in PARSE_TIME_NO_ROUND. ++ * ++ * PARSE_TIME_ROUND_UP_INCLUSIVE: Same as PARSE_TIME_ROUND_UP, minus ++ * one second, unless the most accurate unit specified by 's' is ++ * seconds. This is useful for callers that require a value for ++ * inclusive comparison of the result. ++ * ++ * 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 *ref, int round); ++ ++#ifdef __cplusplus ++} ++#endif ++ ++#endif /* PARSE_TIME_STRING_H */ +-- +1.7.10.4 + -- 2.26.2