osd-contiki/apps/time/time.c
Ralf Schlatterbeck 0068611b4d Implement localtime
Now we manage a timezone and daylight-savings aware version of
localtime. We parse UNIX timezone strings. The default (active after the
first call to localtime or localtime_r) is CET/CEST, the timezone of
Europe/Vienna. The wallclock-time osd-example demonstrates how to set a
different timezone via the timezone resource.

Note: After startup no timezone is set. So in this state querying the
timezone resource will return an empty string. After first call to
localtime (if not timezone has been set via the timezone resource) a
query to timezone will return the default timezone string for CET/CEST.

The string returned by the localtime and utc timezones now also includes
the timezone name.

New fields tm_gmtoff and tm_zone were added to the tm structure. These
are available in BSD systems and when setting special compiler
definitions on Linux.

Note: the timezone offset information in the tm structure (tm_gmtoff)
as well as in the tz structure returned by gettimeofday (tz_minuteswest)
may be wrong sign, this code is largely untested.
2016-02-18 09:55:07 +01:00

848 lines
22 KiB
C

/**
* \addgroup Time related functions
*
* @{
*/
#include "contiki.h"
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "contiki-lib.h"
#include "time.h"
#include "tzparse.h"
#define SECSPERMIN 60
#define MINSPERHOUR 60
#define HOURSPERDAY 24
#define DAYSPERWEEK 7
#define DAYSPERNYEAR 365
#define DAYSPERLYEAR 366
#define SECSPERHOUR (SECSPERMIN * MINSPERHOUR)
#define SECSPERDAY ((long) SECSPERHOUR * HOURSPERDAY)
#define MONSPERYEAR 12
static const int mon_lengths[2][MONSPERYEAR] =
{ { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
, { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
};
/*
* Static timezone information
*/
static struct tzoffset_info localtime_tzoffset;
/* Used for gmtime and localtime, according to manpage on linux the
* internal value may be overwritten "by subsequent calls to any of the
* date and time functions".
*/
static struct tm tm;
/*
* Internal variables to manage offset of utc from the contiki clock
* and timezone offset from utc in minutes.
* For now we don't manage the sub-second offset -- time of setting the
* clock and the precisiont of the clock in use don't warrant this
* effort.
* The last_seconds is used to check if we had a seconds overflow,
* although this happens only every 136 years :-)
*/
static time_t clock_offset;
static uint32_t last_seconds;
static time_t
transtime (time_t janfirst, int year, const struct tzrule *rp, long offset);
# define LEAP_YEAR(_year) \
((_year % 4) == 0 && (_year % 100 != 0 || _year % 400 == 0))
# define YDAYS(_year) (LEAP_YEAR(year) ? 366 : 365)
struct tm *gmtime_r (const time_t *timep, struct tm *ptm)
{
unsigned int year;
int days, month, month_len;
time_t t = *timep;
ptm->tm_sec = t % 60;
t /= 60;
ptm->tm_min = t % 60;
t /= 60;
ptm->tm_hour = t % 24;
t /= 24;
ptm->tm_wday = (t+4) % 7;
year = 1970;
days = 0;
while ((days += YDAYS (year)) <= t)
{
year++;
}
ptm->tm_year = year - 1900;
days -= YDAYS (year);
t -= days;
ptm->tm_yday = t;
for (month=0; month<12; month++)
{
if (month == 1)
{
month_len = LEAP_YEAR (year) ? 29 : 28;
}
else
{
int m = month;
if (m >= 7)
{
m -= 1;
}
m &= 1;
month_len = m ? 30 : 31;
}
if (t >= month_len)
{
t -= month_len;
}
else
{
break;
}
}
ptm->tm_mon = month;
ptm->tm_mday = t + 1;
ptm->tm_isdst = 0;
ptm->tm_gmtoff = 0;
ptm->tm_zone = "UTC";
return ptm;
}
struct tm *gmtime (const time_t *timep)
{
return gmtime_r (timep, &tm);
}
/*
* Compute is_dst flag of given timestamp
*/
static int is_dst (const time_t *timep, const struct tzoffset_info *tzo)
{
time_t janfirst = 0;
time_t starttime, endtime;
int year = 1970;
int lastdst = 0;
if (tzo->dstname == NULL) {
return 0;
}
for (year = 1970; janfirst < *timep; year++) {
starttime = transtime (janfirst, year, &tzo->start, tzo->stdoffset);
endtime = transtime (janfirst, year, &tzo->end, tzo->dstoffset);
if (starttime <= *timep && endtime <= *timep) {
lastdst = (starttime > endtime);
} else if (starttime > *timep && endtime <= *timep) {
return 0;
} else if (starttime <= *timep && endtime > *timep) {
return 1;
} else if (starttime > *timep && endtime > *timep) {
return lastdst;
}
janfirst += YDAYS (year) * SECSPERDAY;
}
return lastdst;
}
struct tm *localtime_r (const time_t *timep, struct tm *ptm)
{
const struct tzoffset_info *tzo = &localtime_tzoffset;
int isdst = 0;
long offset = 0;
time_t t = *timep;
if (tzo->stdname == NULL) {
set_tz (DEFAULT_TIMEZONE);
}
isdst = is_dst (timep, tzo);
offset = isdst ? tzo->dstoffset : tzo->stdoffset;
t -= offset;
gmtime_r (&t, ptm);
ptm->tm_isdst = isdst;
ptm->tm_gmtoff = -offset;
ptm->tm_zone = isdst ? tzo->dstname : tzo->stdname;
return ptm;
}
struct tm *localtime (const time_t *timep)
{
return localtime_r (timep, &tm);
}
/**
* \brief Get time in seconds and microseconds
* gettimeofday will return the clock time as the microseconds part
* while settimeofday will *ignore* the microseconds part (for now).
* Note that the contiki clock interface is broken anyway, we can't read
* seconds and sub-seconds atomically. We try to work around this by
* repeatedly reading seconds, sub-seconds, seconds until first and
* second read of seconds match.
*/
int gettimeofday (struct timeval *tv, struct timezone *tz)
{
uint32_t cs;
if (tv) {
int i;
/* Limit tries to get the same second twice to two */
for (i=0; i<2; i++) {
cs = clock_seconds ();
if (cs < last_seconds) {
clock_offset += 0xFFFFFFFFL;
clock_offset ++;
}
last_seconds = cs;
tv->tv_sec = cs + clock_offset;
tv->tv_usec = ((time_t)(clock_time () % CLOCK_SECOND))
* 1000000L / CLOCK_SECOND;
if (cs == clock_seconds ()) {
break;
}
}
}
if (tz) {
const struct tzoffset_info *tzo = &localtime_tzoffset;
tz->tz_dsttime = is_dst (&tv->tv_sec, tzo);
if (tz->tz_dsttime) {
tz->tz_minuteswest = -tzo->dstoffset / 60;
} else {
tz->tz_minuteswest = -tzo->stdoffset / 60;
}
}
return 0;
}
/**
* \brief Set time in seconds, microseconds ignored for now
*/
int settimeofday (const struct timeval *tv, const struct timezone *tz)
{
/* Don't allow setting timezone */
if (tz) {
errno = ERANGE;
return -1;
}
if (tv) {
uint32_t cs;
cs = clock_seconds ();
clock_offset = tv->tv_sec - cs;
}
return 0;
}
/*
* Save timezone names into reserved string buffer and fill in the names
* into the given tzoffset_info.
* Return -1 on error, 0 for success.
*/
static int save_tznames
( const char *stdname, const char *dstname
, size_t stdlen, size_t dstlen
, struct tzoffset_info *tzo
)
{
size_t len = stdlen;
if (stdname == NULL) {
return -1;
}
if (dstname != NULL) {
len += dstlen;
}
if (len + 2 > sizeof (tzo->namebuf)) {
return -1;
}
tzo->stdname = tzo->namebuf;
strncpy (tzo->namebuf, stdname, stdlen);
tzo->namebuf [stdlen] = '\0';
if (dstlen) {
strncpy (tzo->namebuf + stdlen + 1, dstname, dstlen);
tzo->namebuf [stdlen + 1 + dstlen] = '\0';
tzo->dstname = tzo->namebuf + stdlen + 1;
} else {
tzo->dstname = NULL;
}
return 0;
}
/*
* Utility functions for timezone string parsing (POSIX section 8)
* Code adapted from OpenBSD localtime.c 1.57 2015/12/12 21:25:44
* which is in the public domain.
*/
/*
* The DST rules to use if TZ has no rules:
* We default to US rules as of 1999-08-17.
* POSIX 1003.1 section 8.1.1 says that the default DST rules are
* implementation dependent; for historical reasons, US rules are a
* common default.
*/
#ifndef TZDEFRULESTRING
#define TZDEFRULESTRING ",M4.1.0,M10.5.0"
#endif
/*
* Given the Epoch-relative time of January 1, 00:00:00 UTC, in a year, the
* year, a rule, and the offset from UTC at the time that rule takes effect,
* calculate the Epoch-relative time that rule takes effect.
*/
static time_t
transtime(time_t janfirst, int year, const struct tzrule *rulep, long offset)
{
int leapyear;
time_t value;
int i;
int d, m1, yy0, yy1, yy2, dow;
value = 0;
leapyear = LEAP_YEAR (year);
switch (rulep->r_type) {
case JULIAN_DAY:
/*
* Jn - Julian day, 1 == January 1, 60 == March 1 even in leap
* years.
* In non-leap years, or if the day number is 59 or less, just
* add SECSPERDAY times the day number-1 to the time of
* January 1, midnight, to get the day.
*/
value = janfirst + (rulep->r_day - 1) * SECSPERDAY;
if (leapyear && rulep->r_day >= 60) {
value += SECSPERDAY;
}
break;
case DAY_OF_YEAR:
/*
* n - day of year.
* Just add SECSPERDAY times the day number to the time of
* January 1, midnight, to get the day.
*/
value = janfirst + rulep->r_day * SECSPERDAY;
break;
case MONTH_NTH_DAY_OF_WEEK:
/*
* Mm.n.d - nth "dth day" of month m.
*/
value = janfirst;
for (i = 0; i < rulep->r_mon - 1; ++i) {
value += mon_lengths [leapyear][i] * SECSPERDAY;
}
/*
** Use Zeller's Congruence to get day-of-week of first day of
** month.
*/
m1 = (rulep->r_mon + 9) % 12 + 1;
yy0 = (rulep->r_mon <= 2) ? (year - 1) : year;
yy1 = yy0 / 100;
yy2 = yy0 % 100;
dow = ((26 * m1 - 2) / 10 + 1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7;
if (dow < 0) {
dow += DAYSPERWEEK;
}
/*
** "dow" is the day-of-week of the first day of the month. Get
** the day-of-month (zero-origin) of the first "dow" day of the
** month.
*/
d = rulep->r_day - dow;
if (d < 0) {
d += DAYSPERWEEK;
}
for (i = 1; i < rulep->r_week; ++i) {
if (d + DAYSPERWEEK >= mon_lengths[leapyear][rulep->r_mon - 1]) {
break;
}
d += DAYSPERWEEK;
}
/*
** "d" is the day-of-month (zero-origin) of the day we want.
*/
value += d * SECSPERDAY;
break;
}
/*
** "value" is the Epoch-relative time of 00:00:00 UTC on the day in
** question. To get the Epoch-relative time of the specified local
** time on that day, add the transition time and the current offset
** from UTC.
*/
return value + rulep->r_time + offset;
}
/*
* Given a pointer into a time zone string, scan until a character that is not
* a valid character in a zone name is found. Return a pointer to that
* character.
*/
static const char *getzname (const char *s)
{
char c;
while ((c = *s) != '\0' && !isdigit(c) && c != ',' && c != '-' && c != '+'){
++s;
}
return s;
}
/*
* Given a pointer into an extended time zone string, scan until the ending
* delimiter of the zone name is located. Return a pointer to the delimiter.
*
* As with getzname above, the legal character set is actually quite
* restricted, with other characters producing undefined results.
* We don't do any checking here; checking is done later in common-case code.
*/
static const char *getqzname (const char *strp, const int delim)
{
int c;
while ((c = *strp) != '\0' && c != delim) {
++strp;
}
return strp;
}
/*
* Given a pointer into a time zone string, extract a number from that string.
* Check that the number is within a specified range; if it is not, return
* NULL.
* Otherwise, return a pointer to the first character not part of the number.
*/
static const char *getnum (const char *strp, int *nump, int min, int max)
{
char c;
int num;
if (strp == NULL || !isdigit ((c = *strp))) {
return NULL;
}
num = 0;
do {
num = num * 10 + (c - '0');
if (num > max) {
return NULL; /* illegal value */
}
c = *++strp;
} while (isdigit (c));
if (num < min) {
return NULL; /* illegal value */
}
*nump = num;
return strp;
}
/*
* Given a pointer into a time zone string, extract a number of seconds,
* in hh[:mm[:ss]] form, from the string.
* If any error occurs, return NULL.
* Otherwise, return a pointer to the first character not part of the number
* of seconds.
*/
static const char *getsecs (const char *strp, long *secsp)
{
int num;
/*
* `HOURSPERDAY * DAYSPERWEEK - 1' allows quasi-Posix rules like
* "M10.4.6/26", which does not conform to Posix,
* but which specifies the equivalent of
* ``02:00 on the first Sunday on or after 23 Oct''.
*/
strp = getnum (strp, &num, 0, HOURSPERDAY * DAYSPERWEEK - 1);
if (strp == NULL) {
return NULL;
}
*secsp = num * (long) SECSPERHOUR;
if (*strp == ':') {
++strp;
strp = getnum (strp, &num, 0, MINSPERHOUR - 1);
if (strp == NULL) {
return NULL;
}
*secsp += num * SECSPERMIN;
if (*strp == ':') {
++strp;
/* `SECSPERMIN' allows for leap seconds. */
strp = getnum (strp, &num, 0, SECSPERMIN);
if (strp == NULL) {
return NULL;
}
*secsp += num;
}
}
return strp;
}
/*
* Given a pointer into a time zone string, extract an offset, in
* [+-]hh[:mm[:ss]] form, from the string.
* If any error occurs, return NULL.
* Otherwise, return a pointer to the first character not part of the time.
*/
static const char *getoffset (const char *strp, long *offsetp)
{
int neg = 0;
if (*strp == '-') {
neg = 1;
++strp;
} else if (*strp == '+') {
++strp;
}
strp = getsecs (strp, offsetp);
if (strp == NULL) {
return NULL; /* illegal time */
}
if (neg) {
*offsetp = -*offsetp;
}
return strp;
}
/*
* Parse (optionally extended) timezone name. Return pointer to
* (undelimited) timezone name in tzn and length of same in len.
* Return pointer to first character *after* name, NULL on error.
* Factored from original tzparse function.
*/
static const char *
egettzname (const char *strp, size_t *len, const char **tzn)
{
*tzn = strp;
if (*strp == '<') {
strp++;
*tzn = strp;
strp = getqzname (strp, '>');
if (*strp != '>') {
return NULL;
}
*len = strp - *tzn;
strp++;
} else {
strp = getzname (strp);
*len = strp - *tzn;
}
return strp;
}
/*
** Given a pointer into a time zone string, extract a rule in the form
** date[/time]. See POSIX section 8 for the format of "date" and "time".
** If a valid rule is not found, return NULL.
** Otherwise, return a pointer to the first character not part of the rule.
*/
static const char *getrule (const char *strp, struct tzrule *rulep)
{
if (*strp == 'J') {
/*
* Julian day.
*/
rulep->r_type = JULIAN_DAY;
++strp;
strp = getnum (strp, &rulep->r_day, 1, DAYSPERNYEAR);
} else if (*strp == 'M') {
/*
* Month, week, day.
*/
rulep->r_type = MONTH_NTH_DAY_OF_WEEK;
++strp;
strp = getnum (strp, &rulep->r_mon, 1, MONSPERYEAR);
if (strp == NULL) {
return NULL;
}
if (*strp++ != '.') {
return NULL;
}
strp = getnum (strp, &rulep->r_week, 1, 5);
if (strp == NULL) {
return NULL;
}
if (*strp++ != '.') {
return NULL;
}
strp = getnum (strp, &rulep->r_day, 0, DAYSPERWEEK - 1);
} else if (isdigit (*strp)) {
/*
* Day of year.
*/
rulep->r_type = DAY_OF_YEAR;
strp = getnum (strp, &rulep->r_day, 0, DAYSPERLYEAR - 1);
} else {
return NULL; /* invalid format */
}
if (strp == NULL) {
return NULL;
}
if (*strp == '/') {
/*
* Time specified.
*/
++strp;
strp = getsecs (strp, &rulep->r_time);
} else {
rulep->r_time = 2 * SECSPERHOUR; /* default = 2:00:00 */
}
return strp;
}
/*
* Parse POSIX section 8 TZ string.
* We keep the misnomer "name" for the timezone string.
*/
int tzparse (const char *name, struct tzoffset_info *tzo)
{
const char *stdname;
const char *dstname;
size_t stdlen;
size_t dstlen;
long stdoffset;
long dstoffset;
dstname = NULL;
stdname = name;
name = egettzname (name, &stdlen, &stdname);
if (name == NULL || *name == '\0') {
return -1;
}
name = getoffset (name, &stdoffset);
if (name == NULL) {
return -1;
}
if (*name != '\0') {
name = egettzname (name, &dstlen, &dstname);
if (name == NULL) {
return -1;
}
if (*name != '\0' && *name != ',' && *name != ';') {
name = getoffset (name, &dstoffset);
if (name == NULL) {
return -1;
}
} else {
dstoffset = stdoffset - SECSPERHOUR;
}
if (*name == '\0') {
name = TZDEFRULESTRING;
}
if (*name == ',' || *name == ';') {
struct tzrule start;
struct tzrule end;
++name;
if ((name = getrule (name, &start)) == NULL) {
return -1;
}
if (*name++ != ',') {
return -1;
}
if ((name = getrule (name, &end)) == NULL) {
return -1;
}
if (*name != '\0') {
return -1;
}
if (save_tznames (stdname, dstname, stdlen, dstlen, tzo) != 0) {
return -1;
}
tzo->start = start;
tzo->end = end;
tzo->stdoffset = stdoffset;
tzo->dstoffset = dstoffset;
} else {
return -1;
}
} else {
/* only standard time, no DST */
if (save_tznames (stdname, NULL, stdlen, 0, tzo) != 0) {
return -1;
}
tzo->stdoffset = stdoffset;
tzo->dstoffset = stdoffset;
}
return 0;
}
/*
* Provide a single static timezone which is used by localtime et.al.
*/
int set_tz (const char *tzstring)
{
return tzparse (tzstring, &localtime_tzoffset);
}
static size_t lensecs (long seconds)
{
size_t len = 1;
long secs = abs (seconds);
if (seconds < 0) {
len++;
}
if (secs / 3600 > 9) {
len++;
}
if (secs % 3600) {
len += 3;
if (secs % 60) {
len += 3;
}
}
return len;
}
/*
* Get length of string resulting from serializing rule.
*/
static size_t lenrule (const struct tzrule *rule)
{
size_t len = 0;
if (rule->r_type == JULIAN_DAY) {
len++;
}
if (rule->r_type == JULIAN_DAY || rule->r_type == DAY_OF_YEAR) {
len++;
if (rule->r_day > 9) {
len++;
if (rule->r_day > 99) {
len++;
}
}
} else if (rule->r_type == MONTH_NTH_DAY_OF_WEEK) {
len++;
len++;
if (rule->r_mon > 9) {
len++;
}
len += 4; /* dots and week/day */
if (rule->r_time != 7200) {
len++;
len += lensecs (rule->r_time);
}
}
return len;
}
static int is_extended_name (const char *name)
{
int i;
for (i=0; name [i]; i++) {
if (isdigit (name [i]) || name [i] == '+' || name [i] == '-') {
return 1;
}
}
return 0;
}
/*
* Get length of timezone string resulting from serializing tzo.
*/
static size_t len_tz_r (const struct tzoffset_info *tzo)
{
size_t len = 0;
if (tzo->stdname == NULL) {
return 0;
}
len = strlen (tzo->stdname);
if (is_extended_name (tzo->stdname)) {
len += 2;
}
len += lensecs (tzo->stdoffset);
if (tzo->dstname) {
len += strlen (tzo->dstname);
if (is_extended_name (tzo->dstname)) {
len += 2;
}
if (tzo->dstoffset - tzo->stdoffset != -3600) {
len += lensecs (tzo->dstoffset);
}
len += 2; /* commas */
len += lenrule (&tzo->start);
len += lenrule (&tzo->end);
}
return len;
}
size_t len_tz (void)
{
return len_tz_r (&localtime_tzoffset);
}
void appendsecs (char *buf, long minutes)
{
char *p = buf + strlen (buf);
long min = abs (minutes);
if (minutes < 0) {
*p++ = '-';
}
if (min % 3600 == 0) {
sprintf (p, "%ld", min / 3600);
} else if (min % 60 == 0) {
sprintf (p, "%ld:%ld", min / 3600, (min / 60) % 60);
} else {
sprintf (p, "%ld:%ld:%ld", min / 3600, (min / 60) % 60, min % 60);
}
}
void appendrule (char *buf, const struct tzrule *rule)
{
char *p = buf + strlen (buf);
if (rule->r_type == JULIAN_DAY) {
sprintf (p, "J%d", rule->r_day);
} else if (rule->r_type == DAY_OF_YEAR) {
sprintf (p, "%d", rule->r_day);
} else if (rule->r_type == MONTH_NTH_DAY_OF_WEEK) {
sprintf (p, "M%d.%d.%d", rule->r_mon, rule->r_week, rule->r_day);
p = buf + strlen (buf);
if (rule->r_time != 7200) {
*p++ = '/';
*p = '\0';
appendsecs (p, rule->r_time);
}
}
}
const char *get_tz (char *buf, size_t buflen)
{
const struct tzoffset_info *tzo = &localtime_tzoffset;
if (tzo->stdname == NULL || len_tz_r (tzo) > buflen) {
return NULL;
}
if (is_extended_name (tzo->stdname)) {
sprintf (buf, "<%s>", tzo->stdname);
} else {
strcpy (buf, tzo->stdname);
}
appendsecs (buf, tzo->stdoffset);
if (tzo->dstname != NULL) {
if (is_extended_name (tzo->dstname)) {
sprintf (buf + strlen (buf), "<%s>", tzo->dstname);
} else {
strcat (buf, tzo->dstname);
}
if (tzo->dstoffset - tzo->stdoffset != -3600) {
appendsecs (buf, tzo->dstoffset);
}
strcat (buf, ",");
appendrule (buf, &tzo->start);
strcat (buf, ",");
appendrule (buf, &tzo->end);
}
return buf;
}
/** @} */