The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
 *
 * Some functions (mini_mktime, strftime_xs) are borrowed from 
 * Matt Sergeant's Time::Object module
 *
 * $Id: Date.xs,v b7a562a12fb9 2003/08/16 12:41:29 dlux $
 *
 */

#ifdef __cplusplus
#extern "C" {
#endif
#include "config.h"
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include <time.h>

/* These lines are borrowed from the perl #9823 patch */
/* Mac OSX does not include a definition of tzname in a .h file */
#if defined (HAS_TZNAME) && defined(__APPLE__) && defined(__MACH__)
extern char *tzname[2];
#endif

#define BUFFER_SIZE 254

char buffer[BUFFER_SIZE];

#if 0
#define CLASSDATE_TM_DEBUG(x) strftime(buffer, BUFFER_SIZE-1, "%Z", &x); \
    printf("TZ: %5s, %4s,%4s, %02d:%02d:%02d, %02d-%02d-%03d %1d,%03d,%1d,%s:%s\n", \
        getenv("TZ"), tzname[0], tzname[1], \
        x.tm_sec, x.tm_min, x.tm_hour, x.tm_mday, x.tm_mon, x.tm_year, x.tm_wday, x.tm_yday,x.tm_isdst, \
        asctime(&x), buffer)
#else
#define CLASSDATE_TM_DEBUG(x)
#endif

#ifdef __cplusplus
}
#endif

/* XXX struct tm on some systems (SunOS4/BSD) contains extra (non POSIX)
 * fields for which we don't have Configure support yet:
 *   char *tm_zone;   -- abbreviation of timezone name
 *   long tm_gmtoff;  -- offset from GMT in seconds
 * To workaround core dumps from the uninitialised tm_zone we get the
 * system to give us a reasonable struct to copy.  This fix means that
 * strftime uses the tm_zone and tm_gmtoff values returned by
 * localtime(time()). That should give the desired result most of the
 * time. But probably not always!
 *
 * This is a temporary workaround to be removed once Configure
 * support is added and NETaa14816 is considered in full.
 * It does not address tzname aspects of NETaa14816.
 */
#ifdef HAS_GNULIBC
# ifndef STRUCT_TM_HASZONE
#    define STRUCT_TM_HASZONE
# endif
#endif

/*
 * classdate_mini_mktime - normalise struct tm values without the localtime()
 * semantics (and overhead) of mktime().
 */
static void
classdate_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;
}

MODULE = Class::Date	PACKAGE = Class::Date

void
strftime_xs(fmt, sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1)
	char *		fmt
	int		sec
	int		min
	int		hour
	int		mday
	int		mon
	int		year
	int		wday
	int		yday
	int		isdst
    PPCODE:
	{
	    char tmpbuf[128];
	    struct tm mytm;
	    int len;
	    memset(&mytm, 0, sizeof(mytm));
	    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;
	    classdate_mini_mktime(&mytm);
	    CLASSDATE_TM_DEBUG(mytm);
	    len = strftime(tmpbuf, sizeof tmpbuf, 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 < sizeof(tmpbuf)) || (len == 0 && *fmt == '\0'))
		ST(0) = sv_2mortal(newSVpv(tmpbuf, len));
            else {
		/* Possibly buf overflowed - try again with a bigger buf */
                int     fmtlen = strlen(fmt);
		int	bufsize = fmtlen + sizeof(tmpbuf);
		char* 	buf;
		int	buflen;

		New(0, buf, bufsize, char);
		while (buf) {
		    CLASSDATE_TM_DEBUG(mytm);
		    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);
		}
		if (buf) {
		    ST(0) = sv_2mortal(newSVpv(buf, buflen));
		    Safefree(buf);
		}
                else
		    ST(0) = sv_2mortal(newSVpv(tmpbuf, len));
	    }
	}
	XSRETURN(1);

MODULE = Class::Date	PACKAGE = Class::Date

void
tzset_xs()
CODE:
    tzset();

MODULE = Class::Date	PACKAGE = Class::Date

void
tzname_xs()
PPCODE:
    EXTEND(SP,2);
    PUSHs(sv_2mortal(newSVpv(tzname[0],0)));
    PUSHs(sv_2mortal(newSVpv(tzname[1],0)));
    XSRETURN(2);