/* POSIX::strftime::GNU::XS - XS extension for POSIX::strftime::GNU
*
* Copyright (c) 2012 Piotr Roszatycki <dexter@cpan.org>.
*
* This is free software; you can redistribute it and/or modify it under
* the same terms as perl itself.
*/
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#define NEED_sv_2pv_flags
#include "ppport.h"
size_t gnu_strftime (char *s, size_t maxsize, const char *format, const struct tm *tp);
/* Based on util.c
*
* Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001,
* 2002, 2003, 2004, 2005, 2006, 2007, 2008 by Larry Wall and others
*
* You may distribute under the terms of either the GNU General Public
* License or the Artistic License, as specified in the README file.
*
*/
#ifndef init_tm
# ifdef HAVE_TM_ZONE
static void
init_tm(struct tm *ptm) /* see mktime, strftime and asctime */
{
Time_t now;
(void)time(&now);
Copy(localtime(&now), ptm, 1, struct tm);
}
# else
# define init_tm(ptm)
# endif
#endif
#ifndef mini_mktime
/*
* mini_mktime - normalise struct tm values without the localtime()
* semantics (and overhead) of mktime().
*/
static void
mini_mktime(struct tm *ptm)
{
int yearday;
int secs;
int month, mday, year, jday;
int odd_cent, odd_year;
#define DAYS_PER_YEAR 365
#define DAYS_PER_QYEAR (4*DAYS_PER_YEAR+1)
#define DAYS_PER_CENT (25*DAYS_PER_QYEAR-1)
#define DAYS_PER_QCENT (4*DAYS_PER_CENT+1)
#define SECS_PER_HOUR (60*60)
#define SECS_PER_DAY (24*SECS_PER_HOUR)
/* parentheses deliberately absent on these two, otherwise they don't work */
#define MONTH_TO_DAYS 153/5
#define DAYS_TO_MONTH 5/153
/* offset to bias by March (month 4) 1st between month/mday & year finding */
#define YEAR_ADJUST (4*MONTH_TO_DAYS+1)
/* as used here, the algorithm leaves Sunday as day 1 unless we adjust it */
#define WEEKDAY_BIAS 6 /* (1+6)%7 makes Sunday 0 again */
/*
* Year/day algorithm notes:
*
* With a suitable offset for numeric value of the month, one can find
* an offset into the year by considering months to have 30.6 (153/5) days,
* using integer arithmetic (i.e., with truncation). To avoid too much
* messing about with leap days, we consider January and February to be
* the 13th and 14th month of the previous year. After that transformation,
* we need the month index we use to be high by 1 from 'normal human' usage,
* so the month index values we use run from 4 through 15.
*
* Given that, and the rules for the Gregorian calendar (leap years are those
* divisible by 4 unless also divisible by 100, when they must be divisible
* by 400 instead), we can simply calculate the number of days since some
* arbitrary 'beginning of time' by futzing with the (adjusted) year number,
* the days we derive from our month index, and adding in the day of the
* month. The value used here is not adjusted for the actual origin which
* it normally would use (1 January A.D. 1), since we're not exposing it.
* We're only building the value so we can turn around and get the
* normalised values for the year, month, day-of-month, and day-of-year.
*
* For going backward, we need to bias the value we're using so that we find
* the right year value. (Basically, we don't want the contribution of
* March 1st to the number to apply while deriving the year). Having done
* that, we 'count up' the contribution to the year number by accounting for
* full quadracenturies (400-year periods) with their extra leap days, plus
* the contribution from full centuries (to avoid counting in the lost leap
* days), plus the contribution from full quad-years (to count in the normal
* leap days), plus the leftover contribution from any non-leap years.
* At this point, if we were working with an actual leap day, we'll have 0
* days left over. This is also true for March 1st, however. So, we have
* to special-case that result, and (earlier) keep track of the 'odd'
* century and year contributions. If we got 4 extra centuries in a qcent,
* or 4 extra years in a qyear, then it's a leap day and we call it 29 Feb.
* Otherwise, we add back in the earlier bias we removed (the 123 from
* figuring in March 1st), find the month index (integer division by 30.6),
* and the remainder is the day-of-month. We then have to convert back to
* 'real' months (including fixing January and February from being 14/15 in
* the previous year to being in the proper year). After that, to get
* tm_yday, we work with the normalised year and get a new yearday value for
* January 1st, which we subtract from the yearday value we had earlier,
* representing the date we've re-built. This is done from January 1
* because tm_yday is 0-origin.
*
* Since POSIX time routines are only guaranteed to work for times since the
* UNIX epoch (00:00:00 1 Jan 1970 UTC), the fact that this algorithm
* applies Gregorian calendar rules even to dates before the 16th century
* doesn't bother me. Besides, you'd need cultural context for a given
* date to know whether it was Julian or Gregorian calendar, and that's
* outside the scope for this routine. Since we convert back based on the
* same rules we used to build the yearday, you'll only get strange results
* for input which needed normalising, or for the 'odd' century years which
* were leap years in the Julian calander but not in the Gregorian one.
* I can live with that.
*
* This algorithm also fails to handle years before A.D. 1 gracefully, but
* that's still outside the scope for POSIX time manipulation, so I don't
* care.
*/
year = 1900 + ptm->tm_year;
month = ptm->tm_mon;
mday = ptm->tm_mday;
/* allow given yday with no month & mday to dominate the result */
if (ptm->tm_yday >= 0 && mday <= 0 && month <= 0) {
month = 0;
mday = 0;
jday = 1 + ptm->tm_yday;
}
else {
jday = 0;
}
if (month >= 2)
month+=2;
else
month+=14, year--;
yearday = DAYS_PER_YEAR * year + year/4 - year/100 + year/400;
yearday += month*MONTH_TO_DAYS + mday + jday;
/*
* Note that we don't know when leap-seconds were or will be,
* so we have to trust the user if we get something which looks
* like a sensible leap-second. Wild values for seconds will
* be rationalised, however.
*/
if ((unsigned) ptm->tm_sec <= 60) {
secs = 0;
}
else {
secs = ptm->tm_sec;
ptm->tm_sec = 0;
}
secs += 60 * ptm->tm_min;
secs += SECS_PER_HOUR * ptm->tm_hour;
if (secs < 0) {
if (secs-(secs/SECS_PER_DAY*SECS_PER_DAY) < 0) {
/* got negative remainder, but need positive time */
/* back off an extra day to compensate */
yearday += (secs/SECS_PER_DAY)-1;
secs -= SECS_PER_DAY * (secs/SECS_PER_DAY - 1);
}
else {
yearday += (secs/SECS_PER_DAY);
secs -= SECS_PER_DAY * (secs/SECS_PER_DAY);
}
}
else if (secs >= SECS_PER_DAY) {
yearday += (secs/SECS_PER_DAY);
secs %= SECS_PER_DAY;
}
ptm->tm_hour = secs/SECS_PER_HOUR;
secs %= SECS_PER_HOUR;
ptm->tm_min = secs/60;
secs %= 60;
ptm->tm_sec += secs;
/* done with time of day effects */
/*
* The algorithm for yearday has (so far) left it high by 428.
* To avoid mistaking a legitimate Feb 29 as Mar 1, we need to
* bias it by 123 while trying to figure out what year it
* really represents. Even with this tweak, the reverse
* translation fails for years before A.D. 0001.
* It would still fail for Feb 29, but we catch that one below.
*/
jday = yearday; /* save for later fixup vis-a-vis Jan 1 */
yearday -= YEAR_ADJUST;
year = (yearday / DAYS_PER_QCENT) * 400;
yearday %= DAYS_PER_QCENT;
odd_cent = yearday / DAYS_PER_CENT;
year += odd_cent * 100;
yearday %= DAYS_PER_CENT;
year += (yearday / DAYS_PER_QYEAR) * 4;
yearday %= DAYS_PER_QYEAR;
odd_year = yearday / DAYS_PER_YEAR;
year += odd_year;
yearday %= DAYS_PER_YEAR;
if (!yearday && (odd_cent==4 || odd_year==4)) { /* catch Feb 29 */
month = 1;
yearday = 29;
}
else {
yearday += YEAR_ADJUST; /* recover March 1st crock */
month = yearday*DAYS_TO_MONTH;
yearday -= month*MONTH_TO_DAYS;
/* recover other leap-year adjustment */
if (month > 13) {
month-=14;
year++;
}
else {
month-=2;
}
}
ptm->tm_year = year - 1900;
if (yearday) {
ptm->tm_mday = yearday;
ptm->tm_mon = month;
}
else {
ptm->tm_mday = 31;
ptm->tm_mon = month - 1;
}
/* re-build yearday based on Jan 1 to get tm_yday */
year--;
yearday = year*DAYS_PER_YEAR + year/4 - year/100 + year/400;
yearday += 14*MONTH_TO_DAYS + 1;
ptm->tm_yday = jday - yearday;
/* fix tm_wday if not overridden by caller */
if ((unsigned)ptm->tm_wday > 6)
ptm->tm_wday = (jday + WEEKDAY_BIAS) % 7;
}
#endif
static char *
my_gnu_strftime(pTHX_ const char *fmt, int sec, int min, int hour, int mday, int mon, int year, int wday, int yday, int isdst)
{
char *buf;
int buflen;
struct tm mytm;
int len;
#ifdef PERL_ARGS_ASSERT_MY_STRFTIME
PERL_ARGS_ASSERT_MY_STRFTIME;
#endif
init_tm(&mytm); /* XXX workaround - see init_tm() above */
mytm.tm_sec = sec;
mytm.tm_min = min;
mytm.tm_hour = hour;
mytm.tm_mday = mday;
mytm.tm_mon = mon;
mytm.tm_year = year;
mytm.tm_wday = wday;
mytm.tm_yday = yday;
mytm.tm_isdst = isdst;
mini_mktime(&mytm);
/* use libc to get the values for tm_gmtoff and tm_zone [perl #18238] */
#if defined(HAVE_MKTIME) && (defined(HAVE_TM_GMTOFF) || defined(HAVE_TM_ZONE))
STMT_START {
struct tm mytm2;
mytm2 = mytm;
mktime(&mytm2);
#ifdef HAVE_TM_GMTOFF
mytm.tm_gmtoff = mytm2.tm_gmtoff;
#endif
#ifdef HAVE_TM_ZONE
mytm.tm_zone = mytm2.tm_zone;
#endif
} STMT_END;
#endif
buflen = 64;
Newx(buf, buflen, char);
len = gnu_strftime(buf, buflen, fmt, &mytm);
/*
** The following is needed to handle to the situation where
** tmpbuf overflows. Basically we want to allocate a buffer
** and try repeatedly. The reason why it is so complicated
** is that getting a return value of 0 from strftime can indicate
** one of the following:
** 1. buffer overflowed,
** 2. illegal conversion specifier, or
** 3. the format string specifies nothing to be returned(not
** an error). This could be because format is an empty string
** or it specifies %p that yields an empty string in some locale.
** If there is a better way to make it portable, go ahead by
** all means.
*/
if ((len > 0 && len < buflen) || (len == 0 && *fmt == '\0'))
return buf;
else {
/* Possibly buf overflowed - try again with a bigger buf */
const int fmtlen = strlen(fmt);
int bufsize = fmtlen + buflen;
Renew(buf, bufsize, char);
while (buf) {
buflen = strftime(buf, bufsize, fmt, &mytm);
if (buflen > 0 && buflen < bufsize)
break;
/* heuristic to prevent out-of-memory errors */
if (bufsize > 100*fmtlen) {
Safefree(buf);
buf = NULL;
break;
}
bufsize *= 2;
Renew(buf, bufsize, char);
}
return buf;
}
}
MODULE = POSIX::strftime::GNU::XS PACKAGE = POSIX::strftime::GNU::XS
void
xs_strftime(fmt, sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1)
SV * fmt
int sec
int min
int hour
int mday
int mon
int year
int wday
int yday
int isdst
CODE:
{
#if (PERL_BCDVERSION >= 0x5011001)
char *buf = my_gnu_strftime(aTHX_ SvPV_nolen(fmt), sec, min, hour, mday, mon, year, wday, yday, isdst);
if (buf) {
SV *const sv = sv_newmortal();
sv_usepvn_flags(sv, buf, strlen(buf), SV_HAS_TRAILING_NUL);
if (SvUTF8(fmt)) {
SvUTF8_on(sv);
}
ST(0) = sv;
}
#else
char *buf = my_gnu_strftime(aTHX_ SvPV_nolen(fmt), sec, min, hour, mday, mon, year, wday, yday, isdst);
if (buf) {
ST(0) = sv_2mortal(newSVpv(buf, 0));
Safefree(buf);
}
#endif
}