[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

Candidate 'getdate' replacement.

From: C. Scott Ananian <cananian_at_lesser-magoo.lcs.mit.edu>
Date: 2001-08-23 01:50:22 CEST

So here's the first draft of my candidate replacement for 'getdate'.
The raw C file is below; a .tgz which contains a wrapper so that you
can test the function by executing, for example:
 
 % ./svn_date "yesterday"
 Tue, 21 Aug 2001 19:23:08 -0400
 % ./svn_date "2 days ago at 5pm"
 Mon, 20 Aug 2001 17:00:00 -0400

is attached.

Disclaimers and things to brag about:
 1) I haven't attempted to put this in SVN's C/indentation style at all.
    I just banged this out for a first-look. Likewise, several parts
    are more obscure/less commented than they would be if I was trying
    to get this committed to CVS. It's just a huge 1-day hack that
    nevertheless is functionality-complete. If you like it, I'll clean
    it up.
 2) The interface to the date parser is the same as the one currently
    in svn_time.h. This is non-optimal, because it does not allow
    returning that special time one second before the epoch (it uses
    -1 as an error value). But you could, in theory, plug this
    code right into SVN now if you wanted to.
 3) We always try to parse as ISO8601 first. (A copy of the ISO 8601
    spec is included in the attached .tgz.) We don't support week/day
    numbers (1985-W15-5) because they make me shudder. It wouldn't
    actually be hard to support them. We support the W3C subset of
    ISO8601 (see http://www.w3.org/TR/NOTE-datetime) completely.
    I would recommend that ISO 8601 dates are the only ones guaranteed
    in the SVN manual to be supported in the future and in differing
    locales. A nifty feature is that we support reading in ISO-8601
    format dates using non-roman digit sets if the locale and the c
    library cooperate. I.e, you can use "real" arabic numerals. =)
 4) We then try some fancy natural language processing. No ambiguous
    formats are represented. 4/3/5 is *not* a valid input, regardless
    of your locale. But most of the phrases accepted by the current
    getdate *are*. We can also read in RFC822-conformant dates.
    ("Wed, 22 Aug 2001 19:35:16 -0400") and relative dates ("next
    thursday"). All non-ISO8601 dates are in *local* time. Thus,
    the date specification "8pm" is 8pm for *you*, not 20:00 UTC.
    You can specify a time zone ("GMT" or "UTC" for example) to
    force UTC interpretation. I do *NOT* currently support the
    'three-letter' time zone codes, like EST, EDT, PST, etc.
    Time zones in ISO8601 format ("+08:00", "-0400") and military
    time zone letters ("A"-"Z", excluding "J"; see
    http://www.eyrx.com/DOC/D_GNUTIL/GSU_3.HTM#SEC7 ) are accepted.
    This is not a fundamental limitation; I'm just trying to avoid
    accepting potentially ambiguous descriptors ('EST' has different
    meanings in Australia and the US, for example).
 5) The code is fully localizable. We will accept any locale-specific
    day/month abbreviations, and the code uses gettext() [please, no
    flamewars, this is not a GPL issue, please see today's thread titled
    "working"] to allow translations of the various "yesterday", "now",
    "today", "N days ago" phrases to be installed. I'd love to get
    some non-U.S. feedback here to see if it all does work like it
    should.
 6) Performance will *suck* compared to getdate. A bison or yacc parser
    is a highly-optimized state machine. This is more of a brute-force
    approach where we try different possible input formats over and over
    again until one sticks. This is okay (I told myself) because this
    routine should only be used for command-line parsing. Performance
    shouldn't matter. Besides, localizing a highly-optimized state
    machine would be a nightmare. So there are some trade-offs required.
    It's fast enough for interactive use, but don't use it in an inner
    loop.
 7) See http://www.eyrx.com/DOC/D_GNUTIL/GSU_3.HTM and the ISO 8601
    spec for some ideas of things to throw at it. The main routine
    passes argv[1] to svn_parse_date, so you may have to quote your
    date/time string, and the date parser only returns success if it
    manages to match the *entire* string (no unknown garbage left over).
    Any amount of leading, trailing, or delimiting spaces should be
    fine, though.

So what else? Take a look at the implementation below, untar the
attachment and try some strings, and let me know if you discover any
funnies. Enjoy!
 --s
p.s. you'll notice that i'm in the habit of trimming my .sig when I've
appended something I'd like people to look at. don't worry, it's not gone
for good, it will be back. ;-p

Kojarena payment East Timor terrorist Columbia Delta Force Saddam Hussein
Cheney NRA Treasury President nuclear SEAL Team 6 TASS postcard operative
              ( http://lesser-magoo.lcs.mit.edu/~cananian )

------ begin svn_date.c ---------
------ (don't bother cutting here, this is just for looking at) ---
------ (use the .tgz attached if you really want to play w/ this) ---
#define _XOPEN_SOURCE /* make sure to get strptime */
#include <time.h>
#include <ctype.h> /* for isdigit, isspace */
#include <string.h> /* for memset, etc */
#include <stdlib.h> /* for atoi */
#include <stdio.h> /* for sscanf */
#include <libintl.h> /* for gettext */
#include "svn_date.h"

/* possible date formats we accept */
static int ISO8601ext_parse(const char *text, const time_t now,
                            time_t *resultp);
static int ISO8601bas_parse(const char *text, const time_t now,
                            time_t *resultp);
static int locale_parse(const char *text, const time_t now,
                        time_t *resultp);

/* helper functions */
static int ISO8601ext_parse_TZD(const char *text, struct tm *tm);
static int ISO8601bas_parse_TZD(const char *text, struct tm *tm);
static const char * locale_parse_time(const char *text, struct tm *tm);
static const char * locale_parse_date(const char *text, struct tm *tm);
static const char * locale_parse_day(const char *text, struct tm *tm);
static const char * locale_parse_zone(const char *text, struct tm *tm);
static const char * locale_parse_relative(const char *text, struct tm *tm);


/* The one public interface date parser; convert human-readable
   date TEXT into a standard C time_t. Note that 'now' is passed as
   a parameter so that you can use this routine to find out how SVN
   *would have* parsed some string at some *arbitrary* time: relative
   times should always parse the same even if svn_parse_date is called
   multiple times during a computation of finite length. For this reason,
   the 'now' parameter is *mandatory*. */
time_t svn_parse_date (const char * text, const time_t now) {
  time_t result;
  /* this is a succession of candidate parsers. the first one that
   * produces a valid parse of the *complete* string wins. */
  if (ISO8601ext_parse(text, now, &result)) return result;
  if (ISO8601bas_parse(text, now, &result)) return result;
  if (locale_parse(text, now, &result)) return result;
  /* error */
  /* XXX: not a good return value, but this is the current interface */
  return (time_t) -1;
}


/* init tm. */
static void init_tm(struct tm *tm) {
  memset(tm, 0, sizeof(*tm));
  tm->tm_mday = 1; /* tm is not valid with all zeros. */
}
/* convert 'struct tm' *expressed as UTC* into the number of seconds
 * elapsed since 00:00:00 January 1, 1970, UTC */
static time_t mktime_UTC(struct tm *tm) {
  struct tm localepoch_tm;
  time_t localepoch;
  init_tm(&localepoch_tm);
  localepoch_tm.tm_year = 70;
  localepoch = mktime(&localepoch_tm);
  return mktime(tm)-localepoch;
}


/* Parse according to W3C subset of ISO 8601: see
   http://www.w3.org/TR/NOTE-datetime
   The complete date must be specified.
   Any unspecified time components are assumed to be zeros.
   This is the 'extended' format of ISO8601.
   As per W3C subset, we don't allow week and day numbers.
*/
static int ISO8601ext_parse(const char *text, const time_t now,
                            time_t *resultp) {
  struct tm tm; char *cp; time_t result;
  /* GENERAL EXTENSIONS TO W3C SUBSET: leading zeros are optional in
   * year, month, day, hour, minute, and second fields. We also
   * allow the locale's alternative numeric symbols, if any. */

  /* Try: YYYY-MM-DDThh:mm:ss.sTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY-%Om-%OdT%OH:%OM:%OS.", &tm);
  if (cp != NULL && isdigit(*cp)) {
    while (isdigit(*cp)) cp++; /* discard fractional seconds */
    /* handle time-zone designator */
    if (ISO8601ext_parse_TZD(cp, &tm)) goto success;
  }
  /* EXTENSION TO W3C SUBSET: Allow commas as decimal points */
  /* Try: YYYY-MM-DDThh:mm:ss,sTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY-%Om-%OdT%OH:%OM:%OS,", &tm);
  if (cp != NULL && isdigit(*cp)) {
    while (isdigit(*cp)) cp++; /* discard fractional seconds */
    /* handle time-zone designator */
    if (ISO8601ext_parse_TZD(cp, &tm)) goto success;
  }

  /* Try: YYYY-MM-DDThh:mm:ssTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY-%Om-%OdT%OH:%OM:%OS", &tm);
  if (cp != NULL && ISO8601ext_parse_TZD(cp, &tm)) goto success;

  /* Try: YYYY-MM-DDThh:mmTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY-%Om-%OdT%OH:%OM", &tm);
  if (cp != NULL && ISO8601ext_parse_TZD(cp, &tm)) goto success;

  /* Try: YYYY-MM-DD */
  init_tm(&tm);
  cp = strptime(text, " %EY-%Om-%Od ", &tm);
  if (cp!=NULL && *cp==0) goto success;
  /* XXX: sketchy: should this be midnight local or midnight UTC? */

  /* The YYYY and YYYY-MM formats are disallowed as incomplete. */

  /* can't parse as ISO8601 extended format */
  return 0;

 success:
  result = mktime_UTC(&tm);
  if (result == (time_t)-1) return 0; /* failure: invalid date! */
  *resultp = result;
  return 1; /* success */
}

/* time zone designator is: 'Z' or '+hh:mm' or '-hh:mm' */
static int ISO8601ext_parse_TZD(const char *text, struct tm *tm) {
  int isplus = 0;
  switch(*(text++)) {
  case 'Z': break; /* done! */
    break;
  case '+':
    isplus=1;
  case '-':
    /* parse hh:mm */
    if (isdigit(text[0]) && isdigit(text[1]) && (text[2]==':') &&
        isdigit(text[3]) && isdigit(text[4])) {
      /* we jump through hoops to use atoi here, in the hopes that
       * the c-library has localized this function (along with isdigit)
       * to deal with possibly non-roman digit sets */
      char buf[3] = {'0', '0', '\0' }; int hours, minutes;
      buf[0] = text[0]; buf[1] = text[1];
      hours = atoi(buf);
      buf[0] = text[3]; buf[1] = text[4];
      minutes = atoi(buf);
      /* okay, add hours and minutes to tz. mktime will fixup if we exceed
       * the legal range for hours/minutes */
      if (isplus) {
        /* if time specified is in +X time zone, must subtract X to get UTC */
        tm->tm_hour -= hours; tm->tm_min -= minutes;
      } else {
        /* if time specified is in -X time zone, must add X to get UTC */
        tm->tm_hour += hours; tm->tm_min += minutes;
      }
      text+=5; /* skip parsed hh:mm string */
      break; /* done! */
    }
    /* EXTENSION TO W3C SUBSET: allow omitting the minutes section. */
    /* parse hh */
    if (isdigit(text[0]) && isdigit(text[1])) {
      /* we jump through hoops to use atoi here, in the hopes that
       * the c-library has localized this function (along with isdigit)
       * to deal with possibly non-roman digit sets */
      char buf[3] = {'0', '0', '\0' }; int hours;
      buf[0] = text[0]; buf[1] = text[1];
      hours = atoi(buf);
      /* okay, add hours to tz. mktime will fixup if we exceed
       * the legal range for hours */
      if (isplus) {
        /* if time specified is in +X time zone, must subtract X to get UTC */
        tm->tm_hour -= hours;
      } else {
        /* if time specified is in -X time zone, must add X to get UTC */
        tm->tm_hour += hours;
      }
      text+=2; /* skip parsed hh string */
      break; /* done! */
    }
  default:
    return 0; /* fail */
  }
  /* should be only spaces until the end of string */
  while (isspace(*text)) text++; /* skip spaces */
  return (*text=='\0'); /* success if this is the end of string */
}

/* Parse according to 'basic' format of ISO8601.
 * We don't allow week and day numbers.
 */
static int ISO8601bas_parse(const char *text, const time_t now,
                            time_t *resultp) {
  struct tm tm; char *cp; time_t result;
  /* extensions to ISO8601: leading zeros are optional in
   * year, month, day, hour, minute, and second fields. We also
   * allow the locale's alternative numeric symbols, if any. */

  /* Try: YYYYMMDDThhmmss.sTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY%Om%OdT%OH%OM%OS.", &tm);
  if (cp != NULL && isdigit(*cp)) {
    while (isdigit(*cp)) cp++; /* discard fractional seconds */
    /* handle time-zone designator */
    if (ISO8601bas_parse_TZD(cp, &tm)) goto success;
  }
  /* Try: YYYYMMDDThhmmss,sTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY%Om%OdT%OH%OM%OS,", &tm);
  if (cp != NULL && isdigit(*cp)) {
    while (isdigit(*cp)) cp++; /* discard fractional seconds */
    /* handle time-zone designator */
    if (ISO8601bas_parse_TZD(cp, &tm)) goto success;
  }

  /* Try: YYYYMMDDThhmmssTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY%Om%OdT%OH%OM%OS", &tm);
  if (cp != NULL && ISO8601bas_parse_TZD(cp, &tm)) goto success;

  /* Try: YYYYMMDDThhmmTZD */
  init_tm(&tm);
  cp = strptime(text, " %EY%Om%OdT%OH%OM", &tm);
  if (cp != NULL && ISO8601bas_parse_TZD(cp, &tm)) goto success;

  /* Try: YYYYMMDD */
  init_tm(&tm);
  cp = strptime(text, " %EY%Om%Od ", &tm);
  if (cp!=NULL && *cp==0) goto success;
  /* XXX: sketchy: should this be midnight local or midnight UTC? */

  /* The YYYY and YYYYMM formats are disallowed as incomplete. */

  /* can't parse as ISO8601 basic format */
  return 0;

 success:
  result = mktime_UTC(&tm);
  if (result == (time_t)-1) return 0; /* failure: invalid date! */
  *resultp = result;
  return 1; /* success */
}

/* time zone designator is: 'Z' or '+hhmm' or '-hhmm' */
static int ISO8601bas_parse_TZD(const char *text, struct tm *tm) {
  int isplus = 0;
  switch(*(text++)) {
  case 'Z': break; /* done! */
    break;
  case '+':
    isplus=1;
  case '-':
    /* parse hhmm */
    if (isdigit(text[0]) && isdigit(text[1]) &&
        isdigit(text[2]) && isdigit(text[3])) {
      /* we jump through hoops to use atoi here, in the hopes that
       * the c-library has localized this function (along with isdigit)
       * to deal with possibly non-roman digit sets */
      char buf[3] = {'0', '0', '\0' }; int hours, minutes;
      buf[0] = text[0]; buf[1] = text[1];
      hours = atoi(buf);
      buf[0] = text[2]; buf[1] = text[3];
      minutes = atoi(buf);
      /* okay, add hours and minutes to tz. mktime will fixup if we exceed
       * the legal range for hours/minutes */
      if (isplus) {
        /* if time specified is in +X time zone, must subtract X to get UTC */
        tm->tm_hour -= hours; tm->tm_min -= minutes;
      } else {
        /* if time specified is in -X time zone, must add X to get UTC */
        tm->tm_hour += hours; tm->tm_min += minutes;
      }
      text+=4; /* skip parsed hhmm string */
      break; /* done! */
    }
    /* parse hh */
    if (isdigit(text[0]) && isdigit(text[1])) {
      /* we jump through hoops to use atoi here, in the hopes that
       * the c-library has localized this function (along with isdigit)
       * to deal with possibly non-roman digit sets */
      char buf[3] = {'0', '0', '\0' }; int hours;
      buf[0] = text[0]; buf[1] = text[1];
      hours = atoi(buf);
      /* okay, add hours to tz. mktime will fixup if we exceed
       * the legal range for hours */
      if (isplus) {
        /* if time specified is in +X time zone, must subtract X to get UTC */
        tm->tm_hour -= hours;
      } else {
        /* if time specified is in -X time zone, must add X to get UTC */
        tm->tm_hour += hours;
      }
      text+=2; /* skip parsed hh string */
      break; /* done! */
    }
  default:
    return 0; /* fail */
  }
  /* should be only spaces until the end of string */
  while (isspace(*text)) text++; /* skip spaces */
  return (*text=='\0'); /* success if this is the end of string */
}

/* Parse 'human-readable' date, for the current locale. Words not
 * numbers. =) This does absolute time/dates only. All times are
 * *LOCAL* unless time zone is specified.
 */
static int locale_parse(const char *text, const time_t now,
                            time_t *resultp) {
  struct tm tm; const char *cp; time_t result;

  /* initialize tm to 'now' in localtime */
  {
    struct tm *tmp = localtime(&now);
    memcpy(&tm, tmp, sizeof(tm));
  }

  /* parse elements repeatedly until no elements are left (or we find
   * an unparsable string. */
  while (*text!='\0') {
    /* skip leading whitespace */
    while (isspace(*text)) text++;
    if (*text=='\0') break;
    /* now attempt parse */
    if (NULL != (cp = locale_parse_time(text, &tm))) { text=cp; continue; }
    if (NULL != (cp = locale_parse_date(text, &tm))) { text=cp; continue; }
    if (NULL != (cp = locale_parse_day(text, &tm))) { text=cp; continue; }
    if (NULL != (cp = locale_parse_zone(text, &tm))) { text=cp; continue; }
    if (NULL != (cp = locale_parse_relative(text, &tm))) { text=cp; continue; }
    return 0; /* can't parse as 'human-readable' absolute date */
  }

  result = mktime(&tm); /* tm is localtime */
  if (result == (time_t)-1) return 0; /* failure: invalid date! */
  *resultp = result;
  return 1; /* success */
}

static const char * locale_parse_time(const char *text, struct tm *tm) {
  char *cp; struct tm tmcopy = *tm, _tm;
  /* time fields are zero if not explicitly specified */
  tmcopy.tm_sec = tmcopy.tm_min = tmcopy.tm_hour = 0;
  _tm = tmcopy;
  if (NULL!=(cp = strptime(text, " %OI:%OM:%OS %p ", &_tm))) goto success_i;
  _tm = tmcopy;
  if (NULL!=(cp = strptime(text, " %OH:%OM:%OS ", &_tm))) goto success;
  _tm = tmcopy;
  if (NULL!=(cp = strptime(text, " %OI:%OM %p ", &_tm))) goto success_i;
  _tm = tmcopy;
  if (NULL!=(cp = strptime(text, " %OH:%OM ", &_tm))) goto success;
  _tm = tmcopy;
  if (NULL!=(cp = strptime(text, " %OI %p ", &_tm))) goto success_i;
  _tm = tmcopy;
  if (NULL!=(cp = strptime(text, " %EX ", &_tm))) goto success;
  /* failure */
  return NULL;

 success_i:
  /* versions of glibc before 10-aug-2001 contain bug parsing %OI */
  /* see cvs log for revision 1.34 of libc/time/strptime.c */
  /* so test for this bug and work-around it if necessary */
  {
    struct tm _tm_ = tmcopy;
    strptime("8","%OI", &_tm_);
    if (_tm_.tm_hour==7) { /* BUGGY GLIBC! */
      /* fixup the bug, sigh. */
      _tm.tm_hour = _tm.tm_hour+1;
      if (_tm.tm_hour==12 || _tm.tm_hour==24) _tm.tm_hour-=12;
    }
  }
 success:
  *tm = _tm;
  return cp;
}
static const char * locale_parse_date(const char *text, struct tm *tm) {
  char *cp; struct tm tmcopy = *tm, _tm;
  /* all date fields default to current date unless specified. */
  _tm = tmcopy;
  /* 1976-09-27: ISO 8601 */
  if (NULL!=(cp = strptime(text, " %EY-%Om-%Od ", &_tm))) goto success;
  _tm = tmcopy;
  /* 27 September 1976 or 27sep1976, etc */
  if (NULL!=(cp = strptime(text, " %Od %b %EY ", &_tm))) goto success;
  _tm = tmcopy;
  /* 27-September-1976 */
  if (NULL!=(cp = strptime(text, " %Od-%b-%EY ", &_tm))) goto success;
  _tm = tmcopy;
  /* 27 sep */
  if (NULL!=(cp = strptime(text, " %Od %b ", &_tm))) goto success;
  _tm = tmcopy;
  /* 27-sep */
  if (NULL!=(cp = strptime(text, " %Od-%b ", &_tm))) goto success;
  _tm = tmcopy;
  /* September-27-1976 */
  if (NULL!=(cp = strptime(text, " %b-%Od-%EY ", &_tm))) goto success;
  _tm = tmcopy;
  /* Sep 27, 1976 */
  if (NULL!=(cp = strptime(text, " %b %Od, %EY ", &_tm))) goto success;
  _tm = tmcopy;
  /* Sep 27 */
  if (NULL!=(cp = strptime(text, " %b %Od ", &_tm))) goto success;
  _tm = tmcopy;
  /* Sep-27 */
  if (NULL!=(cp = strptime(text, " %b-%Od ", &_tm))) goto success;
  _tm = tmcopy;
  /* locale-specific date format */
  if (NULL!=(cp = strptime(text, " %Ex ", &_tm))) goto success;
  /* failure */
  return NULL;

 success:
  *tm = _tm;
  return cp;
}
static const char * locale_parse_day(const char *text, struct tm *tm) {
  char *cp; struct tm _tm = *tm, *tmp; int daydiff=0; time_t time; int i;
  /* phrases like 'first monday', 'last monday', 'next monday' */
  static const struct {
    const char *phrase;
    int weekdiff;
  } phrases[] = {
    /* these phrases should be localized: note that we call gettext below */
    { " last %a ", -1 },
    { " this %a ", 0 },
    { " next %a ", 1 },
    { " first %a ", 1 },
    { " second %a ", 2 },
    { " third %a ", 3 },
    { " fourth %a ", 4 },
    { " fifth %a ", 5 },
    { 0, 0 },
  };
  /* relative days */
  for (i=0 ; phrases[i].phrase!=NULL; i++)
    if (NULL!=(cp = strptime(text, gettext(phrases[i].phrase), &_tm))) {
      daydiff=7*phrases[i].weekdiff;
      goto success;
    }
  /* simple day of the week. */
  if (NULL!=(cp = strptime(text, " %a ", &_tm))) goto success;
  return NULL;

 success:
  /* _tm.tm_wday has the day of the week. Push current date ahead if
   * necessary. */
  time = mktime(tm); tmp = localtime(&time); /* normalize all fields of tm */
  if (_tm.tm_wday != tmp->tm_wday) { /* if selected day is not current day: */
    /* compute the number of days we need to push ahead. */
    daydiff += (7 + _tm.tm_wday - tmp->tm_wday) % 7;
    _tm = *tmp;
    _tm.tm_mday += daydiff; /* increment day of the month */
    time = mktime(&_tm); tmp = localtime(&time); /* normalize fields of _tm */
    *tm = *tmp; /* this is the new date */
  }
  /* a comma following a day of the week item is ignored */
  while (isspace(*cp)) cp++;
  if (*cp==',') cp++;
  /* done! */
  return cp;
}
static const char * locale_parse_zone(const char *text, struct tm *tm) {
  /* this is ugly: we accept '-0100', '+0100', '-01:00', and '+01:00'
   * as well as 'GMT' 'UTC' and 'UT' for UTC. We also allow a single-letter
   * 'military' time zone designation, so long as it is followed by a space.
   * No other time zone designations are allowed; too many ambiguities.
   */
  int i; const char *cp; int minutediff;
  const static struct {
    const char *name;
    int hourdiff;
  } zones[] = {
    { "GMT", 0 },
    { "UTC", 0 },
    { "UT", 0 },
    { "A", 1 },
    { "B", 2 },
    { "C", 3 },
    { "D", 4 },
    { "E", 5 },
    { "F", 6 },
    { "G", 7 },
    { "H", 8 },
    { "I", 9 },
    { "K", 10 },
    { "L", 11 },
    { "M", 12 },
    { "N", -1 },
    { "O", -2 },
    { "P", -3 },
    { "Q", -4 },
    { "R", -5 },
    { "S", -6 },
    { "T", -7 },
    { "U", -8 },
    { "V", -9 },
    { "W", -10 },
    { "X", -11 },
    { "Y", -12 },
    { "Z", 0 },
    { 0, 0 } };
  /* try time zone abbreviations */
  for (i=0; zones[i].name!=NULL; i++) {
    int zonelen = strlen(zones[i].name);
    if (strncmp(text, zones[i].name, zonelen)==0 &&
        (zonelen>1 || isspace(text[zonelen]) || text[zonelen]=='\0')) {
      cp = text+zonelen;
      minutediff = 60*zones[i].hourdiff;
      goto success;
    }
  }
  /* try +0000 +00:00 formats */
  if ((text[0]=='-' || text[0]=='+') &&
      isdigit(text[1]) && isdigit(text[2])) {
    char buf[3] = {'0', '0', '\0' };
    buf[0] = text[1]; buf[1] = text[2];
    minutediff = 60*atoi(buf);
    cp = text+3;
    if (*cp==':') cp++;
    if (isdigit(cp[0]) && isdigit(cp[1])) {
      buf[0] = cp[0]; buf[1] = cp[1];
      minutediff += atoi(buf);
      if (text[0]=='-') minutediff=-minutediff;
      cp+=2;
      goto success;
    }
  }
  /* failure */
  return NULL;

 success:
  /* apply GMT correction */
  {
    struct tm localepoch_tm;
    time_t localepoch;
    init_tm(&localepoch_tm);
    localepoch_tm.tm_year = 70;
    localepoch_tm.tm_isdst = tm->tm_isdst;
    localepoch = mktime(&localepoch_tm);
    tm->tm_sec -= localepoch;
  }
  /* now apply zone correction. */
  tm->tm_min -= minutediff;
  /* renormalize tm */
  {
    time_t time = mktime(tm);
    struct tm *tmp = localtime(&time);
    *tm = *tmp;
  }
  /* done. */
  return cp;
}

/* parse 'relative items' in date string, such as '1 year', '2 years ago',
 * etc. The strings in this function can be localized. */
static const char * locale_parse_relative(const char *text, struct tm *tm) {
  int i;
  /* these phrases should all be translated */
  const static struct {
    const char *phrase;
    int seconds; int months; int years;
  } phrases[] = {
    /* ALL PHRASES MUST END WITH %n */
    /* times in the past */
    { "%d second ago%n", -1, 0, 0 },
    { "%d seconds ago%n", -1, 0, 0 },
    { "%d minute ago%n", -60, 0, 0 },
    { "%d minutes ago%n", -60, 0, 0 },
    { "%d hour ago%n", -60*60, 0, 0 },
    { "%d hours ago%n", -60*60, 0, 0 },
    { "%d day ago%n", -60*60*24, 0, 0 },
    { "%d days ago%n", -60*60*24, 0, 0 },
    { "%d week ago%n", -60*60*24*7, 0, 0 },
    { "%d weeks ago%n", -60*60*24*7, 0, 0 },
    { "%d fortnight ago%n", -60*60*24*7*2, 0, 0 },
    { "%d fortnights ago%n", -60*60*24*7*2, 0, 0 },
    { "%d month ago%n", 0, -1, 0 },
    { "%d months ago%n", 0, -1, 0 },
    { "%d year ago%n", 0, 0, -1 },
    { "%d years ago%n", 0, 0, -1 },
    /* times in the future */
    { "%d second%n", 1, 0, 0 },
    { "%d seconds%n", 1, 0, 0 },
    { "%d minute%n", 60, 0, 0 },
    { "%d minutes%n", 60, 0, 0 },
    { "%d hour%n", 60*60, 0, 0 },
    { "%d hours%n", 60*60, 0, 0 },
    { "%d day%n", 60*60*24, 0, 0 },
    { "%d days%n", 60*60*24, 0, 0 },
    { "%d week%n", 60*60*24*7, 0, 0 },
    { "%d weeks%n", 60*60*24*7, 0, 0 },
    { "%d fortnight%n", 60*60*24*7*2, 0, 0 },
    { "%d fortnights%n", 60*60*24*7*2, 0, 0 },
    { "%d month%n", 0, 1, 0 },
    { "%d months%n", 0, 1, 0 },
    { "%d year%n", 0, 0, 1 },
    { "%d years%n", 0, 0, 1 },
    /* other special times */
    /* NOTE: the %n at the beginning so that all phrases assign two ints */
    { "t%nomorrow%n", 60*60*24, 0, 0 },
    { "y%nesterday%n", -60*60*24, 0, 0 },
    /* the current time */
    { "%ntoday%n", 0, 0, 0 },
    { "%nnow%n", 0, 0, 0 },
    /* nonce words */
    { "%nat%n", 0, 0, 0 },
    { 0, 0, 0, 0 }
  };
  /* relative days */
  for (i=0 ; phrases[i].phrase!=NULL; i++) {
    int n=0, chars = 0;
    sscanf(text, gettext(phrases[i].phrase), &n, &chars);
    if (chars!=0 && (text[chars]=='\0' || isspace(text[chars]))) {
      text+=chars;
      tm->tm_sec += n*phrases[i].seconds;
      tm->tm_mon += n*phrases[i].months;
      tm->tm_year += n*phrases[i].years;
      /* renormalize tm */
      {
        time_t time = mktime(tm);
        struct tm *tmp = localtime(&time);
        *tm = *tmp;
      }
      /* done */
      return text;
    }
  }
  /* failure */
  return NULL;
}

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org

Received on Sat Oct 21 14:36:36 2006

This is an archived mail posted to the Subversion Dev mailing list.

This site is subject to the Apache Privacy Policy and the Apache Public Forum Archive Policy.