/* * R : A Computer Language for Statistical Data Analysis * Copyright (c) 1989 The Regents of the University of California. * Copyright (C) 2013-2014 The R Core Team * * 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, a copy is available at * https://www.R-project.org/Licenses/ */ /* Based on code from tzcode, which is turn said to be 'Based on the UCB version with the copyright notice appearing below.' Extensive changes for use with R. ** Copyright (c) 1989 The Regents of the University of California. ** All rights reserved. ** ** Redistribution and use in source and binary forms are permitted ** provided that the above copyright notice and this paragraph are ** duplicated in all such forms and that any documentation, ** advertising materials, and other materials related to such ** distribution and use acknowledge that the software was developed ** by the University of California, Berkeley. The name of the ** University may not be used to endorse or promote products derived ** from this software without specific prior written permission. ** THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. */ #include #undef HAVE_TM_ZONE #define HAVE_TM_ZONE 1 #undef HAVE_TM_GMTOFF #define HAVE_TM_GMTOFF 1 #include "tzfile.h" #include #include #include #include #include #include // memcpy #include #include // for INT_MAX #include "datetime.h" static char * _add(const char *, char *, const char *); static char * _conv(int, const char *, char *, const char *); static char * _fmt(const char *, const stm *, char *, const char *); static char * _yconv(int, int, int, int, char *, const char *); size_t R_strftime(char * const s, const size_t maxsize, const char *const format, const stm *const t) { char *p; R_tzset(); p = _fmt(((format == NULL) ? "%c" : format), t, s, s + maxsize); if (p == s + maxsize) return 0; *p = '\0'; return p - s; } #ifdef HAVE_NL_LANGINFO // This was part of the configure check # include #else static char *orig(const char *fmt, const stm *const t) { static char buff[100]; // no known name is over 20 bytes struct tm tm; memset(&tm, 0, sizeof(struct tm)); tm.tm_sec = t->tm_sec; tm.tm_min = t->tm_min; tm.tm_hour = t->tm_hour; tm.tm_mday = t->tm_mday; tm.tm_mon = t->tm_mon; tm.tm_year = t->tm_year; tm.tm_wday = t->tm_wday; tm.tm_yday = t->tm_yday; tm.tm_isdst = t->tm_isdst; strftime(buff, 100, fmt, &tm); return buff; } #endif static char * _fmt(const char *format, const stm *const t, char * pt, const char *const ptlim) { for ( ; *format; ++format) { if (*format == '%') { /* Check for POSIX 2008 / GNU modifiers */ char pad = '\0'; const char *f; int width = -1; // first look to see if this is %..Y for (f = format+1; *f ; f++) if(! (isdigit(*f) || *f == '_' || *f == '+')) break; if (*f == 'Y') { while (1) { switch (*++format) { case '0': case '+': // pad with zeroes, and more (not here) case '_': // pad with spaces: GNU extension pad = *format; continue; default: break; } break; } if (isdigit (*format)) { width = 0; do { if (width > INT_MAX / 10 || (width == INT_MAX / 10 && *format - '0' > INT_MAX % 10)) width = INT_MAX; else {width *= 10; width += *format - '0';} format++; } while (isdigit (*format)); } --format; } label: switch (*++format) { case '\0': --format; break; /* now the locale-dependent cases */ #ifdef HAVE_NL_LANGINFO case 'A': pt = _add((t->tm_wday < 0 || t->tm_wday >= 7) ? "?" : nl_langinfo(DAY_1 + t->tm_wday), pt, ptlim); continue; case 'a': pt = _add((t->tm_wday < 0 || t->tm_wday >= 7) ? "?" : nl_langinfo(ABDAY_1 + t->tm_wday), pt, ptlim); continue; case 'B': pt = _add((t->tm_mon < 0 || t->tm_mon >= 12) ? "?" : nl_langinfo(MON_1 + t->tm_mon), pt, ptlim); continue; case 'b': case 'h': pt = _add((t->tm_mon < 0 || t->tm_mon >= 12) ? "?" : nl_langinfo(ABMON_1 + t->tm_mon), pt, ptlim); continue; case 'c': pt = _fmt(nl_langinfo(D_T_FMT), t, pt, ptlim); continue; case 'p': pt = _add(nl_langinfo(t->tm_hour < 12 ? AM_STR : PM_STR), pt, ptlim); continue; case 'P': { char *p = nl_langinfo(t->tm_hour < 12 ? AM_STR : PM_STR), *q, buff[20]; for (q = buff; *p; ) *q++ = (char) tolower(*p++); *q = '\0'; pt = _add(buff, pt, ptlim); } continue; case 'X': pt = _fmt(nl_langinfo(T_FMT), t, pt, ptlim); continue; case 'x': pt = _fmt(nl_langinfo(D_FMT), t, pt, ptlim); continue; #else case 'A': pt = _add((t->tm_wday < 0 || t->tm_wday >= 7) ? "?" : orig("%A", t), pt, ptlim); continue; case 'a': pt = _add((t->tm_wday < 0 || t->tm_wday >= 7) ? "?" : orig("%a", t), pt, ptlim); continue; case 'B': pt = _add((t->tm_mon < 0 || t->tm_mon >= 12) ? "?" : orig("%B", t), pt, ptlim); continue; case 'b': case 'h': pt = _add((t->tm_mon < 0 || t->tm_mon >= 12) ? "?" : orig("%b", t), pt, ptlim); continue; case 'c': // In a C locale this is supposed to be // "%a %b %e %T %Y". It is not on Windows .... #ifdef _WIN32 pt = _fmt("%a %b %e %T %Y", t, pt, ptlim); #else pt = _fmt(orig("%c", t), t, pt, ptlim); #endif continue; case 'p': pt = _add(orig("%p", t), pt, ptlim); continue; case 'P': { char *p = orig("%p", t), *q, buff[20]; for (q = buff; *p; ) *q++ = (char) tolower(*p++); *q = '\0'; pt = _add(buff, pt, ptlim); } continue; case 'X': pt = _fmt(orig("%X", t), t, pt, ptlim); continue; case 'x': pt = _fmt(orig("%x", t), t, pt, ptlim); continue; #endif /* now the locale-independent ones */ case 'C': pt = _yconv(t->tm_year, TM_YEAR_BASE, 1, 0, pt, ptlim); continue; case 'D': pt = _fmt("%m/%d/%y", t, pt, ptlim); continue; case 'd': pt = _conv(t->tm_mday, "%02d", pt, ptlim); continue; case 'E': case 'O': /* ** C99 locale modifiers. ** The sequences ** %Ec %EC %Ex %EX %Ey %EY ** %Od %oe %OH %OI %Om %OM ** %OS %Ou %OU %OV %Ow %OW %Oy ** are supposed to provide alternate ** representations. */ goto label; case 'e': pt = _conv(t->tm_mday, "%2d", pt, ptlim); continue; case 'F': pt = _fmt("%Y-%m-%d", t, pt, ptlim); continue; case 'H': pt = _conv(t->tm_hour, "%02d", pt, ptlim); continue; case 'I': pt = _conv((t->tm_hour % 12) ? (t->tm_hour % 12) : 12, "%02d", pt, ptlim); continue; case 'j': pt = _conv(t->tm_yday + 1, "%03d", pt, ptlim); continue; case 'k': pt = _conv(t->tm_hour, "%2d", pt, ptlim); continue; case 'l': pt = _conv((t->tm_hour % 12) ? (t->tm_hour % 12) : 12, "%2d", pt, ptlim); continue; case 'M': pt = _conv(t->tm_min, "%02d", pt, ptlim); continue; case 'm': pt = _conv(t->tm_mon + 1, "%02d", pt, ptlim); continue; case 'n': pt = _add("\n", pt, ptlim); continue; case 'R': pt = _fmt("%H:%M", t, pt, ptlim); continue; case 'r': pt = _fmt("%I:%M:%S %p", t, pt, ptlim); continue; case 'S': pt = _conv(t->tm_sec, "%02d", pt, ptlim); continue; case 's': { stm tm = *t; char buf[22]; // <= 19 digs + sign + terminator int_fast64_t mkt = R_mktime(&tm); #ifdef _WIN32 // not ISO C99, so warns (void) snprintf(buf, 22, "%I64d", mkt); #else (void) snprintf(buf, 22, "%lld", (long long) mkt); #endif pt = _add(buf, pt, ptlim); } continue; case 'T': pt = _fmt("%H:%M:%S", t, pt, ptlim); continue; case 't': pt = _add("\t", pt, ptlim); continue; case 'U': pt = _conv((t->tm_yday + 7 - t->tm_wday) / 7, "%02d", pt, ptlim); continue; case 'u': pt = _conv((t->tm_wday == 0) ? 7 : t->tm_wday, "%d", pt, ptlim); continue; case 'V': /* ISO 8601 week number */ case 'G': /* ISO 8601 year (four digits) */ case 'g': /* ISO 8601 year (two digits) */ { int year, base, yday, wday, w; year = t->tm_year; base = TM_YEAR_BASE; yday = t->tm_yday; wday = t->tm_wday; for ( ; ; ) { int len, bot, top; len = isleap_sum(year, base) ? DAYSPERLYEAR : DAYSPERNYEAR; /* ** What yday (-3 ... 3) does ** the ISO year begin on? */ bot = ((yday + 11 - wday) % 7) - 3; /* ** What yday does the NEXT ** ISO year begin on? */ top = bot - (len % 7); if (top < -3) top += 7; top += len; if (yday >= top) { ++base; w = 1; break; } if (yday >= bot) { w = 1 + ((yday - bot) / 7); break; } --base; yday += isleap_sum(year, base) ? DAYSPERLYEAR : DAYSPERNYEAR; } if (*format == 'V') pt = _conv(w, "%02d", pt, ptlim); else if (*format == 'g') pt = _yconv(year, base, 0, 1, pt, ptlim); else // %G pt = _yconv(year, base, 1, 1, pt, ptlim); } continue; case 'v': pt = _fmt("%e-%b-%Y", t, pt, ptlim); continue; case 'W': pt = _conv((t->tm_yday + 7 - (t->tm_wday ? (t->tm_wday - 1) : (7 - 1))) / 7, "%02d", pt, ptlim); continue; case 'w': pt = _conv(t->tm_wday, "%d", pt, ptlim); continue; case 'y': pt = _yconv(t->tm_year, TM_YEAR_BASE, 0, 1, pt, ptlim); continue; case 'Y': // pt = _yconv(t->tm_year, TM_YEAR_BASE, 1, 1, pt, ptlim); { char buf[20] = "%"; int year = TM_YEAR_BASE + t->tm_year; if (pad == '\0') { pad = '0'; width = 4;} if (pad == '0' || pad == '+') strcat(buf, "0"); if (width > 0) sprintf(buf+strlen(buf), "%u", width); if (pad == '+' && year > 9999) strcat(buf, "+"); strcat(buf, "d"); pt = _conv(year, buf, pt, ptlim); } continue; case 'Z': #ifdef HAVE_TM_ZONE if (t->tm_zone != NULL) pt = _add(t->tm_zone, pt, ptlim); else #endif if (t->tm_isdst >= 0) pt = _add(R_tzname[t->tm_isdst != 0], pt, ptlim); /* ** C99 says that %Z must be replaced by the ** empty string if the time zone is not ** determinable. */ continue; case 'z': { long diff; char const *sign; if (t->tm_isdst < 0) continue; #ifdef HAVE_TM_GMTOFF diff = t->tm_gmtoff; #else diff = R_timegm(t) - R_mktime(t); #endif if (diff < 0) { sign = "-"; diff = -diff; } else sign = "+"; pt = _add(sign, pt, ptlim); diff /= SECSPERMIN; diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR); pt = _conv((int) diff, "%04d", pt, ptlim); } continue; case '+': pt = _fmt("%a %b %e %H:%M:%S %Z %Y", t, pt, ptlim); continue; case '%': default: break; } } if (pt == ptlim) break; *pt++ = *format; } return pt; } static char * _conv(const int n, const char *const format, char *const pt, const char *const ptlim) { char buf[12]; (void) snprintf(buf, 12, format, n); return _add(buf, pt, ptlim); } static char * _add(const char *str, char *pt, const char *const ptlim) { while (pt < ptlim && (*pt = *str++) != '\0') ++pt; return pt; } /* ** POSIX and the C Standard are unclear or inconsistent about ** what %C and %y do if the year is negative or exceeds 9999. ** Use the convention that %C concatenated with %y yields the ** same output as %Y, and that %Y contains at least 4 bytes, ** with more only if necessary. * Explained in POSIX 2008, at least. */ static char * _yconv(const int a, const int b, const int convert_top, const int convert_yy, char *pt, const char *const ptlim) { int lead, trail; #define DIVISOR 100 trail = a % DIVISOR + b % DIVISOR; lead = a / DIVISOR + b / DIVISOR + trail / DIVISOR; trail %= DIVISOR; if (trail < 0 && lead > 0) { trail += DIVISOR; --lead; } else if (lead < 0 && trail > 0) { trail -= DIVISOR; ++lead; } if (convert_top) { if (lead == 0 && trail < 0) pt = _add("-0", pt, ptlim); else pt = _conv(lead, "%02d", pt, ptlim); } if (convert_yy) pt = _conv(((trail < 0) ? -trail : trail), "%02d", pt, ptlim); return pt; }