/*
* 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_DEBUG
#define NDEBUG /* for assert() */
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "parse-time-string.h"
#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
/* field indices in struct state tm, and set fields */
enum field {
/* keep SEC...YEAR in this order */
TM_ABS_SEC, /* seconds */
TM_ABS_MIN, /* minutes */
TM_ABS_HOUR, /* hours */
TM_ABS_MDAY, /* day of the month */
TM_ABS_MON, /* month */
TM_ABS_YEAR, /* year */
TM_ABS_WDAY, /* day of the week. special: may be relative */
TM_ABS_ISDST, /* daylight saving time */
TM_AMPM, /* am vs. pm */
TM_TZ, /* timezone in minutes */
/* keep SEC...YEAR in this order */
TM_REL_SEC, /* seconds relative to now */
TM_REL_MIN, /* minutes ... */
TM_REL_HOUR, /* hours ... */
TM_REL_DAY, /* days ... */
TM_REL_MON, /* months ... */
TM_REL_YEAR, /* years ... */
TM_REL_WEEK, /* weeks ... */
TM_NONE, /* not a field */
TM_SIZE = TM_NONE,
};
enum field_set {
FIELD_UNSET,
FIELD_SET,
FIELD_NOW,
};
static enum field
next_field (enum field field)
{
/* note: depends on the enum ordering */
return field < TM_ABS_YEAR ? field + 1 : TM_NONE;
}
static enum field
abs_to_rel_field (enum field field)
{
assert (field <= TM_ABS_YEAR);
/* note: depends on the enum ordering */
return field + (TM_REL_SEC - TM_ABS_SEC);
}
/* get zero value for field */
static int
field_zero (enum field field)
{
if (field == TM_ABS_MDAY || field == TM_ABS_MON)
return 1;
else if (field == TM_ABS_YEAR)
return 1970;
else
return 0;
}
struct state {
int tm[TM_SIZE]; /* parsed date and time */
enum field_set set[TM_SIZE]; /* set status of tm */
enum field last_field;
char delim;
int postponed_length; /* number of digits in postponed value */
int postponed_value;
};
/*
* Helpers for postponed numbers.
*
* postponed_length is the number of digits in postponed value. 0
* means there is no postponed number. -1 means there is a postponed
* number, but it comes from a keyword, and it doesn't have digits.
*/
static int
get_postponed_length (struct state *state)
{
return state->postponed_length;
}
static bool
get_postponed_number (struct state *state, int *v, int *n)
{
if (!state->postponed_length)
return false;
if (n)
*n = state->postponed_length;
if (v)
*v = state->postponed_value;
state->postponed_length = 0;
state->postponed_value = 0;
return true;
}
/* parse postponed number if one exists */
static int parse_postponed_number (struct state *state, int v, int n);
static int
handle_postponed_number (struct state *state)
{
int v = state->postponed_value;
int n = state->postponed_length;
if (!n)
return 0;
state->postponed_value = 0;
state->postponed_length = 0;
return parse_postponed_number (state, v, n);
}
/*
* set new postponed number to be handled later. if one exists
* already, handle it first. n may be -1 to indicate a keyword that
* has no number length.
*/
static int
set_postponed_number (struct state *state, int v, int n)
{
int r;
/* parse previous postponed number, if any */
r = handle_postponed_number (state);
if (r)
return r;
state->postponed_length = n;
state->postponed_value = v;
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.
*/
/* returns unset for non-tracked fields */
static bool
is_field_set (struct state *state, enum field field)
{
assert (field < ARRAY_SIZE (state->tm));
return field < ARRAY_SIZE (state->set) &&
state->set[field] != 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. */
static int
set_field (struct state *state, enum field field, int value)
{
int r;
assert (field < ARRAY_SIZE (state->tm));
/* some fields can only be set once */
if (field < ARRAY_SIZE (state->set) && state->set[field] != FIELD_UNSET)
return -PARSE_TIME_ERR_ALREADYSET;
state->set[field] = FIELD_SET;
/*
* REVISIT: There could be a "next_field" that would be set from
* "field" for the duration of the handle_postponed_number() call,
* so it has more information to work with.
*/
/* parse postponed number, if any */
r = handle_postponed_number (state);
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 current date/time in the
* specified time zone, or local timezone if not specified. The fields
* will be initialized after parsing is complete and timezone is
* known.
*/
static int
set_fields_to_now (struct state *state, enum field *fields, size_t n)
{
size_t i;
int r;
for (i = 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. */
static int
mod_field (struct state *state, enum field field, int value)
{
int r;
assert (field < ARRAY_SIZE (state->tm)); /* assert relative??? */
if (field < ARRAY_SIZE (state->set))
state->set[field] = FIELD_SET;
/* parse postponed number, if any */
r = handle_postponed_number (state);
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];
}
/* 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 */
size_t minlen; /* min length to match, 0 = must match all */
enum field field; /* field to set, or FIELD_NONE if N/A */
int value; /* value to set, or 0 if N/A */
setter_t set; /* function to use for setting, if non-NULL */
};
/*
* Setter callback functions for keywords.
*/
static int
kw_set_default (struct state *state, struct keyword *kw)
{
return set_field (state, kw->field, kw->value);
}
static int
kw_set_rel (struct state *state, struct keyword *kw)
{
int multiplier = 1;
/* get a previously set multiplier, if any */
get_postponed_number (state, &multiplier, NULL);
/* accumulate relative field values */
return mod_field (state, kw->field, multiplier * kw->value);
}
static int
kw_set_number (struct state *state, struct keyword *kw)
{
/* -1 = 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 */
if (n == 1 || n == 2) {
int r, v;
get_postponed_number (state, &v, NULL);
if (v < 1 || v > 31)
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 */
if (n == 1 || n == 2) {
int r, v;
get_postponed_number (state, &v, NULL);
if (v < 1 || v > 12)
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, 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, 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 (!get_postponed_number (state, &v, &n))
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 && (v < 1 || v > 31))
return -PARSE_TIME_ERR_INVALIDDATE;
return set_field (state, TM_ABS_MDAY, v);
}
/*
* Accepted keywords.
*
* If keyword begins with upper case letter, then the matching will be
* case sensitive. Otherwise the matching is case insensitive.
*
* If setter is NULL, set_default will be used.
*
* Note: Order matters. Matching is greedy, longest match is used, but
* of equal length matches the first one is used.
*/
static struct keyword keywords[] = {
/* weekdays */
{ "sunday", 3, TM_ABS_WDAY, 0, NULL },
{ "monday", 3, TM_ABS_WDAY, 1, NULL },
{ "tuesday", 3, TM_ABS_WDAY, 2, NULL },
{ "wednesday", 3, TM_ABS_WDAY, 3, NULL },
{ "thursday", 3, TM_ABS_WDAY, 4, NULL },
{ "friday", 3, TM_ABS_WDAY, 5, NULL },
{ "saturday", 3, TM_ABS_WDAY, 6, NULL },
/* months */
{ "january", 3, TM_ABS_MON, 1, kw_set_month },
{ "february", 3, TM_ABS_MON, 2, kw_set_month },
{ "march", 3, TM_ABS_MON, 3, kw_set_month },
{ "april", 3, TM_ABS_MON, 4, kw_set_month },
{ "may", 3, TM_ABS_MON, 5, kw_set_month },
{ "june", 3, TM_ABS_MON, 6, kw_set_month },
{ "july", 3, TM_ABS_MON, 7, kw_set_month },
{ "august", 3, TM_ABS_MON, 8, kw_set_month },
{ "september", 3, TM_ABS_MON, 9, kw_set_month },
{ "october", 3, TM_ABS_MON, 10, kw_set_month },
{ "november", 3, TM_ABS_MON, 11, kw_set_month },
{ "december", 3, TM_ABS_MON, 12, kw_set_month },
/* durations */
{ "years", 1, TM_REL_YEAR, 1, kw_set_rel },
{ "weeks", 1, TM_REL_WEEK, 1, kw_set_rel },
{ "days", 1, TM_REL_DAY, 1, kw_set_rel },
{ "hours", 1, TM_REL_HOUR, 1, kw_set_rel },
{ "hrs", 1, TM_REL_HOUR, 1, kw_set_rel },
/* M=months, m=minutes. single M must precede minutes in the list. */
{ "M", 1, TM_REL_MON, 1, kw_set_rel },
{ "minutes", 1, TM_REL_MIN, 1, kw_set_rel },
{ "mins", 1, TM_REL_MIN, 1, kw_set_rel },
{ "months", 1, TM_REL_MON, 1, kw_set_rel },
{ "seconds", 1, TM_REL_SEC, 1, kw_set_rel },
{ "secs", 1, TM_REL_SEC, 1, kw_set_rel },
/* numbers */
{ "one", 0, TM_NONE, 1, kw_set_number },
{ "two", 0, TM_NONE, 2, kw_set_number },
{ "three", 0, TM_NONE, 3, kw_set_number },
{ "four", 0, TM_NONE, 4, kw_set_number },
{ "five", 0, TM_NONE, 5, kw_set_number },
{ "six", 0, TM_NONE, 6, kw_set_number },
{ "seven", 0, TM_NONE, 7, kw_set_number },
{ "eight", 0, TM_NONE, 8, kw_set_number },
{ "nine", 0, TM_NONE, 9, kw_set_number },
{ "ten", 0, TM_NONE, 10, kw_set_number },
{ "dozen", 0, TM_NONE, 12, kw_set_number },
{ "hundred", 0, TM_NONE, 100, kw_set_number },
/* special number forms */
{ "this", 0, TM_NONE, 0, kw_set_number },
{ "last", 0, TM_NONE, 1, kw_set_number },
/* specials */
{ "yesterday", 0, TM_REL_DAY, 1, kw_set_rel },
{ "today", 0, TM_NONE, 0, kw_set_today },
{ "now", 0, TM_NONE, 0, kw_set_now },
{ "noon", 0, TM_NONE, 12, kw_set_timeofday },
{ "midnight", 0, TM_NONE, 0, kw_set_timeofday },
{ "am", 0, TM_AMPM, 0, kw_set_ampm },
{ "a.m.", 0, TM_AMPM, 0, kw_set_ampm },
{ "pm", 0, TM_AMPM, 1, kw_set_ampm },
{ "p.m.", 0, TM_AMPM, 1, kw_set_ampm },
{ "st", 0, TM_NONE, 0, kw_set_ordinal },
{ "nd", 0, TM_NONE, 0, kw_set_ordinal },
{ "rd", 0, TM_NONE, 0, kw_set_ordinal },
{ "th", 0, TM_NONE, 0, kw_set_ordinal },
/* timezone codes: offset in minutes. FIXME: add more codes. */
{ "pst", 0, TM_TZ, -8*60, NULL },
{ "mst", 0, TM_TZ, -7*60, NULL },
{ "cst", 0, TM_TZ, -6*60, NULL },
{ "est", 0, TM_TZ, -5*60, NULL },
{ "ast", 0, TM_TZ, -4*60, NULL },
{ "nst", 0, TM_TZ, -(3*60+30), NULL },
{ "gmt", 0, TM_TZ, 0, NULL },
{ "utc", 0, TM_TZ, 0, NULL },
{ "wet", 0, TM_TZ, 0, NULL },
{ "cet", 0, TM_TZ, 1*60, NULL },
{ "eet", 0, TM_TZ, 2*60, NULL },
{ "fet", 0, TM_TZ, 3*60, NULL },
{ "wat", 0, TM_TZ, 1*60, NULL },
{ "cat", 0, TM_TZ, 2*60, NULL },
{ "eat", 0, TM_TZ, 3*60, NULL },
};
/*
* Compare strings s and keyword. Return number of matching chars on
* match, 0 for no match. Match must be at least n chars (n == 0 all
* of keyword), otherwise it's not a match. Use match_case for case
* sensitive matching.
*/
static size_t
stringcmp (const char *s, const char *keyword, size_t n, bool match_case)
{
size_t i;
for (i = 0; *s && *keyword; i++, s++, keyword++) {
if (match_case) {
if (*s != *keyword)
break;
} else {
if (tolower ((unsigned char) *s) !=
tolower ((unsigned char) *keyword))
break;
}
}
if (n)
return i < n ? 0 : i;
else
return *keyword ? 0 : i;
}
/*
* Parse a keyword. Return < 0 on error, number of parsed chars on
* success.
*/
static ssize_t
parse_keyword (struct state *state, const char *s)
{
unsigned int i;
size_t n, max_n = 0;
struct keyword *kw = NULL;
int r;
/* Match longest keyword */
for (i = 0; i < ARRAY_SIZE (keywords); i++) {
/* Match case if keyword begins with upper case letter. */
bool mcase = isupper ((unsigned char) keywords[i].name[0]);
n = stringcmp (s, keywords[i].name, keywords[i].minlen, mcase);
if (n > max_n) {
max_n = n;
kw = &keywords[i];
}
}
if (!kw)
return -PARSE_TIME_ERR_KEYWORD;
if (kw->set)
r = kw->set (state, kw);
else
r = kw_set_default (state, kw);
return r < 0 ? r : max_n;
}
/*
* Non-keyword parsers and their helpers.
*/
static int
set_user_tz (struct state *state, char sign, int hour, int min)
{
int tz = hour * 60 + min;
assert (sign == '+' || sign == '-');
if (hour < 0 || hour > 14 || min < 0 || min > 60 || min % 15)
return -PARSE_TIME_ERR_INVALIDTIME;
if (sign == '-')
tz = -tz;
return set_field (state, TM_TZ, tz);
}
/*
* Independent parsing of a postponed number when it wasn't consumed
* during parsing of the following token.
*
* This should be able to trust that last_field and next_field are
* right.
*/
static int
parse_postponed_number (struct state *state, int v, int n)
{
/*
* alright, these are really lone, won't affect parsing of
* following items... it's not a multiplier, those have been eaten
* away.
*
* also note numbers eaten away by parse_single_number.
*/
assert (n < 8);
switch (n) {
case 1:
case 2:
/* hour or mday or year */
if (state->last_field == TM_ABS_MON && /* FIXME: written mon! */
!is_field_set (state, TM_ABS_MDAY)) {
return set_field (state, TM_ABS_MDAY, v);
}
break;
case 4:
/* YYYY or +/-HHMM for TZ or HHMM or DDMM */
/* FIXME: state->delim is no longer right for this function!
* why not, it could be! */
if (!is_field_set (state, TM_ABS_YEAR)) {
/* FIXME: check year? */
return set_field (state, TM_ABS_YEAR, v);
}
break;
case 6:
/* FIXME: HHMMSS or DDMMYY */
break;
case -1:
/* REVISIT */
break;
case 3:
case 5:
case 7:
default:
break;
}
return -PARSE_TIME_ERR_FORMAT;
}
/* Parse a single number. Typically postpone parsing until later. */
static int
parse_single_number (struct state *state, unsigned long v,
unsigned long n)
{
assert (n);
/* parse things that can be parsed immediately */
if (n == 8) {
/* YYYYMMDD */
int year = v / 10000;
int mon = (v / 100) % 100;
int mday = v % 100;
if (year < 1970 || mon < 1 || mon > 12 || mday < 1 || mday > 31)
return -PARSE_TIME_ERR_INVALIDDATE;
return set_abs_date (state, year, mon, mday);
} else if (n > 8) {
/* FIXME: seconds since epoch */
return -PARSE_TIME_ERR_FORMAT;
}
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;
}
}
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 && year < 1970)
return -PARSE_TIME_ERR_INVALIDDATE;
if (mon != UNSET && (mon < 1 || mon > 12))
return -PARSE_TIME_ERR_INVALIDDATE;
if (mday != UNSET && (mday < 1 || mday > 31))
return -PARSE_TIME_ERR_INVALIDDATE;
return set_abs_date (state, year, mon, mday);
}
static int
parse_time (struct state *state, char sep,
unsigned long v1, unsigned long v2, unsigned long v3,
size_t n1, size_t n2, size_t n3)
{
assert (is_time_sep (sep));
if ((n1 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 2))
return -PARSE_TIME_ERR_TIMEFORMAT;
/*
* REVISIT: this means it's required to set time *before* being
* able to set timezone
*/
if (is_field_set (state, TM_ABS_HOUR) &&
is_field_set (state, TM_ABS_MIN) &&
n1 == 2 && n2 == 2 && n3 == 0 &&
(state->delim == '+' || state->delim == '-')) {
return set_user_tz (state, state->delim, v1, v2);
}
if (v1 > 24 || v2 > 60 || v3 > 60)
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))) {
sep = *p;
v2 = strtoul_len (p + 1, &p, &n2);
} else {
/* a single number */
r = parse_single_number (state, v1, n1);
if (r)
return r;
return p - s;
}
/* 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). Return < 0 on error, number of parsed chars on
* success.
*/
static ssize_t
parse_delim (struct state *state, const char *s)
{
const char *p = s;
/*
* REVISIT: any actions depending on the first delim after last
* field? what could it be?
*/
/*
* skip non-alpha and non-digit, and store the last for further
* processing
*/
while (*p && !isalnum ((unsigned char) *p)) {
set_delim (state, *p);
p++;
}
return p - s;
}
/*
* Parse a date/time string. Return < 0 on error, number of parsed
* chars on success.
*/
static ssize_t
parse_input (struct state *state, const char *s)
{
const char *p = 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; /* FIXME */
}
p += n;
}
/* parse postponed number, if any */
r = handle_postponed_number (state);
if (r < 0)
return r;
return p - s;
}
/*
* Processing the parsed input.
*/
/*
* Initialize reference time to tm. Use time zone in state if
* specified, otherwise local time. Use now for reference time if
* non-NULL, otherwise current time.
*/
static int
initialize_now (struct state *state, struct tm *tm, const time_t *now)
{
time_t t;
if (now) {
t = *now;
} 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). 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_ABS_WDAY: return tm->tm_wday;
case TM_ABS_ISDST: return tm->tm_isdst;
default:
assert (false);
break;
}
return 0;
}
/* Modify hour according to am/pm setting. */
static int
fixup_ampm (struct state *state)
{
int hour, hdiff = 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 (hour < 1 || hour > 12)
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;
}
mod_field (state, TM_REL_HOUR, -hdiff);
return 0;
}
/* Combine absolute and relative fields, and round. */
static int
create_output (struct state *state, time_t *t_out, const time_t *tnow,
int round)
{
struct tm tm = { 0 };
struct tm now;
enum field f;
int r;
int week_round = PARSE_TIME_NO_ROUND;
r = initialize_now (state, &now, tnow);
if (r)
return r;
/* initialize uninitialized fields to now */
for (f = TM_ABS_SEC; f != TM_NONE; f = next_field (f)) {
if (state->set[f] == FIELD_NOW) {
state->tm[f] = tm_get_field (&now, f);
state->set[f] = FIELD_SET;
}
}
/*
* If MON is set but YEAR is not, refer to past month.
*
* REVISIT: Why are month/week special in this regard? What about
* mday, or time. Should refer to past.
*/
if (is_field_set (state, TM_ABS_MON) &&
!is_field_set (state, TM_ABS_YEAR)) {
if (get_field (state, TM_ABS_MON) >= tm_get_field (&now, TM_ABS_MON))
mod_field (state, TM_REL_YEAR, 1);
}
/*
* If WDAY is set but MDAY is not, we consider WDAY relative
*
* REVISIT: This fails on stuff like "two months ago monday"
* because two months ago wasn't the same day as today. Postpone
* until we know date?
*/
if (is_field_set (state, TM_ABS_WDAY) &&
!is_field_set (state, TM_ABS_MDAY)) {
int wday = get_field (state, TM_ABS_WDAY);
int today = tm_get_field (&now, TM_ABS_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 */
mod_field (state, TM_REL_DAY, rel_days);
unset_field (state, TM_ABS_WDAY);
}
r = fixup_ampm (state);
if (r)
return r;
/*
* Iterate fields from least accurate to most accurate, and set
* unset fields according to requested rounding.
*/
for (f = TM_ABS_SEC; f != TM_NONE; f = next_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)
mod_field (state, r, -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, field_zero (f));
}
}
}
if (!is_field_set (state, f))
set_field (state, f, tm_get_field (&now, f));
}
/* special case: rounding with week accuracy */
if (week_round != 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.
*
* REVISIT: This is probably not a solution that universally
* works. Just make sure DST is not taken into account. We don't
* want rounding to be affected by DST.
*/
tm.tm_isdst = -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_DOWN)
tm.tm_mday -= tm.tm_wday;
else
tm.tm_mday += 7 - tm.tm_wday;
}
/* if TZ specified, convert from TZ to local time for mktime(3) */
if (is_field_set (state, TM_TZ)) {
time_t t = mktime (&tm);
/* from specified TZ to UTC */
tm.tm_min -= get_field (state, TM_TZ);
/* from UTC to local TZ (yes, it's hacky - FIXME) */
tm.tm_sec += difftime (mktime (localtime (&t)), mktime (gmtime (&t)));
}
/* FIXME: check return value, don't set if fail */
*t_out = mktime (&tm);
return 0;
}
/* internally, all errors are < 0. parse_time_string() returns errors > 0. */
#define EXTERNAL_ERR(r) (-r)
int
parse_time_string (const char *s, time_t *t, const time_t *now, int round)
{
struct state state = { { 0 } };
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, now, round);
if (r < 0)
return EXTERNAL_ERR (r);
return 0;
}