| /* |
| * 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 <config.h> |
| |
| #undef HAVE_TM_ZONE |
| #define HAVE_TM_ZONE 1 |
| #undef HAVE_TM_GMTOFF |
| #define HAVE_TM_GMTOFF 1 |
| |
| #include "tzfile.h" |
| #include <fcntl.h> |
| #include <locale.h> |
| #include <time.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> // memcpy |
| #include <ctype.h> |
| #include <limits.h> // 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 <langinfo.h> |
| |
| #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; |
| } |