/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Communicator client code, released
 * March 31, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/*
 * JS date methods.
 */

/*
 * "For example, OS/360 devotes 26 bytes of the permanently
 *  resident date-turnover routine to the proper handling of
 *  December 31 on leap years (when it is Day 366).  That
 *  might have been left to the operator."
 *
 * Frederick Brooks, 'The Second-System Effect'.
 */

#include "jsstddef.h"
#include <ctype.h>
#include <locale.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "jstypes.h"
#include "jsprf.h"
#include "prmjtime.h"
#include "jsutil.h" /* Added by JSIFY */
#include "jsapi.h"
#include "jsconfig.h"
#include "jscntxt.h"
#include "jsdate.h"
#include "jsinterp.h"
#include "jsnum.h"
#include "jsobj.h"
#include "jsstr.h"

/*
 * The JS 'Date' object is patterned after the Java 'Date' object.
 * Here is an script:
 *
 *    today = new Date();
 *
 *    print(today.toLocaleString());
 *
 *    weekDay = today.getDay();
 *
 *
 * These Java (and ECMA-262) methods are supported:
 *
 *     UTC
 *     getDate (getUTCDate)
 *     getDay (getUTCDay)
 *     getHours (getUTCHours)
 *     getMinutes (getUTCMinutes)
 *     getMonth (getUTCMonth)
 *     getSeconds (getUTCSeconds)
 *     getMilliseconds (getUTCMilliseconds)
 *     getTime
 *     getTimezoneOffset
 *     getYear
 *     getFullYear (getUTCFullYear)
 *     parse
 *     setDate (setUTCDate)
 *     setHours (setUTCHours)
 *     setMinutes (setUTCMinutes)
 *     setMonth (setUTCMonth)
 *     setSeconds (setUTCSeconds)
 *     setMilliseconds (setUTCMilliseconds)
 *     setTime
 *     setYear (setFullYear, setUTCFullYear)
 *     toGMTString (toUTCString)
 *     toLocaleString
 *     toString
 *
 *
 * These Java methods are not supported
 *
 *     setDay
 *     before
 *     after
 *     equals
 *     hashCode
 */

/*
 * 11/97 - jsdate.c has been rewritten to conform to the ECMA-262 language
 * definition and reduce dependence on NSPR.  NSPR is used to get the current
 * time in milliseconds, the time zone offset, and the daylight savings time
 * offset for a given time.  NSPR is also used for Date.toLocaleString(), for
 * locale-specific formatting, and to get a string representing the timezone.
 * (Which turns out to be platform-dependent.)
 *
 * To do:
 * (I did some performance tests by timing how long it took to run what
 *  I had of the js ECMA conformance tests.)
 *
 * - look at saving results across multiple calls to supporting
 * functions; the toString functions compute some of the same values
 * multiple times.  Although - I took a quick stab at this, and I lost
 * rather than gained.  (Fractionally.)  Hard to tell what compilers/processors
 * are doing these days.
 *
 * - look at tweaking function return types to return double instead
 * of int; this seems to make things run slightly faster sometimes.
 * (though it could be architecture-dependent.)  It'd be good to see
 * how this does on win32.  (Tried it on irix.)  Types could use a
 * general going-over.
 */

/*
 * Supporting functions - ECMA 15.9.1.*
 */

#define HalfTimeDomain  8.64e15
#define HoursPerDay     24.0
#define MinutesPerDay   (HoursPerDay * MinutesPerHour)
#define MinutesPerHour  60.0
#define SecondsPerDay   (MinutesPerDay * SecondsPerMinute)
#define SecondsPerHour  (MinutesPerHour * SecondsPerMinute)
#define SecondsPerMinute 60.0

#if defined(XP_WIN) || defined(XP_OS2)
/* Work around msvc double optimization bug by making these runtime values; if
 * they're available at compile time, msvc optimizes division by them by
 * computing the reciprocal and multiplying instead of dividing - this loses
 * when the reciprocal isn't representable in a double.
 */
static jsdouble msPerSecond = 1000.0;
static jsdouble msPerDay = SecondsPerDay * 1000.0;
static jsdouble msPerHour = SecondsPerHour * 1000.0;
static jsdouble msPerMinute = SecondsPerMinute * 1000.0;
#else
#define msPerDay        (SecondsPerDay * msPerSecond)
#define msPerHour       (SecondsPerHour * msPerSecond)
#define msPerMinute     (SecondsPerMinute * msPerSecond)
#define msPerSecond     1000.0
#endif

#define Day(t)          floor((t) / msPerDay)

static jsdouble
TimeWithinDay(jsdouble t)
{
    jsdouble result;
    result = fmod(t, msPerDay);
    if (result < 0)
	result += msPerDay;
    return result;
}

#define DaysInYear(y)   ((y) % 4 == 0 && ((y) % 100 || ((y) % 400 == 0))  \
			 ? 366 : 365)

/* math here has to be f.p, because we need
 *  floor((1968 - 1969) / 4) == -1
 */
#define DayFromYear(y)  (365 * ((y)-1970) + floor(((y)-1969)/4.0)            \
			 - floor(((y)-1901)/100.0) + floor(((y)-1601)/400.0))
#define TimeFromYear(y) (DayFromYear(y) * msPerDay)

static jsint
YearFromTime(jsdouble t)
{
    jsint y = (jsint) floor(t /(msPerDay*365.2425)) + 1970;
    jsdouble t2 = (jsdouble) TimeFromYear(y);

    if (t2 > t) {
        y--;
    } else {
        if (t2 + msPerDay * DaysInYear(y) <= t)
            y++;
    }
    return y;
}

#define InLeapYear(t)   (JSBool) (DaysInYear(YearFromTime(t)) == 366)

#define DayWithinYear(t, year) ((intN) (Day(t) - DayFromYear(year)))

/*
 * The following array contains the day of year for the first day of
 * each month, where index 0 is January, and day 0 is January 1.
 */
static jsdouble firstDayOfMonth[2][12] = {
    {0.0, 31.0, 59.0, 90.0, 120.0, 151.0, 181.0, 212.0, 243.0, 273.0, 304.0, 334.0},
    {0.0, 31.0, 60.0, 91.0, 121.0, 152.0, 182.0, 213.0, 244.0, 274.0, 305.0, 335.0}
};

#define DayFromMonth(m, leap) firstDayOfMonth[leap][(intN)m];

static intN
MonthFromTime(jsdouble t)
{
    intN d, step;
    jsint year = YearFromTime(t);
    d = DayWithinYear(t, year);

    if (d < (step = 31))
	return 0;
    step += (InLeapYear(t) ? 29 : 28);
    if (d < step)
	return 1;
    if (d < (step += 31))
	return 2;
    if (d < (step += 30))
	return 3;
    if (d < (step += 31))
	return 4;
    if (d < (step += 30))
	return 5;
    if (d < (step += 31))
	return 6;
    if (d < (step += 31))
	return 7;
    if (d < (step += 30))
	return 8;
    if (d < (step += 31))
	return 9;
    if (d < (step += 30))
	return 10;
    return 11;
}

static intN
DateFromTime(jsdouble t)
{
    intN d, step, next;
    jsint year = YearFromTime(t);
    d = DayWithinYear(t, year);

    if (d <= (next = 30))
	return d + 1;
    step = next;
    next += (InLeapYear(t) ? 29 : 28);
    if (d <= next)
	return d - step;
    step = next;
    if (d <= (next += 31))
	return d - step;
    step = next;
    if (d <= (next += 30))
	return d - step;
    step = next;
    if (d <= (next += 31))
	return d - step;
    step = next;
    if (d <= (next += 30))
	return d - step;
    step = next;
    if (d <= (next += 31))
	return d - step;
    step = next;
    if (d <= (next += 31))
	return d - step;
    step = next;
    if (d <= (next += 30))
	return d - step;
    step = next;
    if (d <= (next += 31))
	return d - step;
    step = next;
    if (d <= (next += 30))
	return d - step;
    step = next;
    return d - step;
}

static intN
WeekDay(jsdouble t)
{
    jsint result;
    result = (jsint) Day(t) + 4;
    result = result % 7;
    if (result < 0)
	result += 7;
    return (intN) result;
}

/* LocalTZA gets set by js_InitDateClass() */
static jsdouble LocalTZA;

static jsdouble
DaylightSavingTA(jsdouble t)
{
    volatile int64 PR_t;
    int64 ms2us;
    int64 offset;
    jsdouble result;

    /* abort if NaN */
    if (JSDOUBLE_IS_NaN(t))
	return t;

    /* put our t in an LL, and map it to usec for prtime */
    JSLL_D2L(PR_t, t);
    JSLL_I2L(ms2us, PRMJ_USEC_PER_MSEC);
    JSLL_MUL(PR_t, PR_t, ms2us);

    offset = PRMJ_DSTOffset(PR_t);

    JSLL_DIV(offset, offset, ms2us);
    JSLL_L2D(result, offset);
    return result;
}


#define AdjustTime(t)   fmod(LocalTZA + DaylightSavingTA(t), msPerDay)

#define LocalTime(t)    ((t) + AdjustTime(t))

static jsdouble
UTC(jsdouble t)
{
    return t - AdjustTime(t - LocalTZA);
}

static intN
HourFromTime(jsdouble t)
{
    intN result = (intN) fmod(floor(t/msPerHour), HoursPerDay);
    if (result < 0)
	result += (intN)HoursPerDay;
    return result;
}

static intN
MinFromTime(jsdouble t)
{
    intN result = (intN) fmod(floor(t / msPerMinute), MinutesPerHour);
    if (result < 0)
	result += (intN)MinutesPerHour;
    return result;
}

static intN
SecFromTime(jsdouble t)
{
    intN result = (intN) fmod(floor(t / msPerSecond), SecondsPerMinute);
    if (result < 0)
	result += (intN)SecondsPerMinute;
    return result;
}

static intN
msFromTime(jsdouble t)
{
    intN result = (intN) fmod(t, msPerSecond);
    if (result < 0)
	result += (intN)msPerSecond;
    return result;
}

#define MakeTime(hour, min, sec, ms) \
(((hour * MinutesPerHour + min) * SecondsPerMinute + sec) * msPerSecond + ms)

static jsdouble
MakeDay(jsdouble year, jsdouble month, jsdouble date)
{
    jsdouble result;
    JSBool leap;
    jsdouble yearday;
    jsdouble monthday;

    year += floor(month / 12);

    month = fmod(month, 12.0);
    if (month < 0)
	month += 12;

    leap = (DaysInYear((jsint) year) == 366);

    yearday = floor(TimeFromYear(year) / msPerDay);
    monthday = DayFromMonth(month, leap);

    result = yearday
	     + monthday
	     + date - 1;
    return result;
}

#define MakeDate(day, time) (day * msPerDay + time)

#define TIMECLIP(d) ((JSDOUBLE_IS_FINITE(d) \
		      && !((d < 0 ? -d : d) > HalfTimeDomain)) \
		     ? js_DoubleToInteger(d + (+0.)) : *cx->runtime->jsNaN)

/**
 * end of ECMA 'support' functions
 */

/*
 * Other Support routines and definitions
 */

static JSClass date_class = {
    js_Date_str,
    JSCLASS_HAS_PRIVATE,
    JS_PropertyStub,  JS_PropertyStub,  JS_PropertyStub,  JS_PropertyStub,
    JS_EnumerateStub, JS_ResolveStub,   JS_ConvertStub,   JS_FinalizeStub,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

/* for use by date_parse */

static const char* wtb[] = {
    "am", "pm",
    "monday", "tuesday", "wednesday", "thursday", "friday",
    "saturday", "sunday",
    "january", "february", "march", "april", "may", "june",
    "july", "august", "september", "october", "november", "december",
    "gmt", "ut", "utc",
    "est", "edt",
    "cst", "cdt",
    "mst", "mdt",
    "pst", "pdt"
    /* time zone table needs to be expanded */
};

static int ttb[] = {
    -1, -2, 0, 0, 0, 0, 0, 0, 0,       /* AM/PM */
    2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
    10000 + 0, 10000 + 0, 10000 + 0,   /* GMT/UT/UTC */
    10000 + 5 * 60, 10000 + 4 * 60,    /* EST/EDT */
    10000 + 6 * 60, 10000 + 5 * 60,    /* CST/CDT */
    10000 + 7 * 60, 10000 + 6 * 60,    /* MST/MDT */
    10000 + 8 * 60, 10000 + 7 * 60     /* PST/PDT */
};

/* helper for date_parse */
static JSBool
date_regionMatches(const char* s1, int s1off, const jschar* s2, int s2off,
		   int count, int ignoreCase)
{
    JSBool result = JS_FALSE;
    /* return true if matches, otherwise, false */

    while (count > 0 && s1[s1off] && s2[s2off]) {
	if (ignoreCase) {
	    if (JS_TOLOWER((jschar)s1[s1off]) != JS_TOLOWER(s2[s2off])) {
		break;
	    }
	} else {
	    if ((jschar)s1[s1off] != s2[s2off]) {
		break;
	    }
	}
	s1off++;
	s2off++;
	count--;
    }

    if (count == 0) {
	result = JS_TRUE;
    }

    return result;
}

/* find UTC time from given date... no 1900 correction! */
static jsdouble
date_msecFromDate(jsdouble year, jsdouble mon, jsdouble mday, jsdouble hour,
		  jsdouble min, jsdouble sec, jsdouble msec)
{
    jsdouble day;
    jsdouble msec_time;
    jsdouble result;

    day = MakeDay(year, mon, mday);
    msec_time = MakeTime(hour, min, sec, msec);
    result = MakeDate(day, msec_time);
    return result;
}

/*
 * See ECMA 15.9.4.[3-10];
 */
/* XXX this function must be above date_parseString to avoid a
   horrid bug in the Win16 1.52 compiler */
#define MAXARGS        7
static JSBool
date_UTC(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble array[MAXARGS];
    uintN loop;
    jsdouble d;

    for (loop = 0; loop < MAXARGS; loop++) {
	if (loop < argc) {
	    if (!js_ValueToNumber(cx, argv[loop], &d))
		return JS_FALSE;
	    /* return NaN if any arg is NaN */
	    if (!JSDOUBLE_IS_FINITE(d)) {
		return js_NewNumberValue(cx, d, rval);
	    }
	    array[loop] = floor(d);
	} else {
	    array[loop] = 0;
	}
    }

    /* adjust 2-digit years into the 20th century */
    if (array[0] >= 0 && array[0] <= 99)
	array[0] += 1900;

    /* if we got a 0 for 'date' (which is out of range)
     * pretend it's a 1.  (So Date.UTC(1972, 5) works) */
    if (array[2] < 1)
	array[2] = 1;

    d = date_msecFromDate(array[0], array[1], array[2],
			      array[3], array[4], array[5], array[6]);
    d = TIMECLIP(d);

    return js_NewNumberValue(cx, d, rval);
}

static JSBool
date_parseString(JSString *str, jsdouble *result)
{
    jsdouble msec;

    const jschar *s = JSSTRING_CHARS(str);
    size_t limit = JSSTRING_LENGTH(str);
    size_t i = 0;
    int year = -1;
    int mon = -1;
    int mday = -1;
    int hour = -1;
    int min = -1;
    int sec = -1;
    int c = -1;
    int n = -1;
    jsdouble tzoffset = -1;  /* was an int, overflowed on win16!!! */
    int prevc = 0;
    JSBool seenplusminus = JS_FALSE;

    if (limit == 0)
	goto syntax;
    while (i < limit) {
	c = s[i];
	i++;
	if (c <= ' ' || c == ',' || c == '-') {
	    if (c == '-' && '0' <= s[i] && s[i] <= '9') {
	      prevc = c;
	    }
	    continue;
	}
	if (c == '(') { /* comments) */
	    int depth = 1;
	    while (i < limit) {
		c = s[i];
		i++;
		if (c == '(') depth++;
		else if (c == ')')
		    if (--depth <= 0)
			break;
	    }
	    continue;
	}
	if ('0' <= c && c <= '9') {
	    n = c - '0';
	    while (i < limit && '0' <= (c = s[i]) && c <= '9') {
		n = n * 10 + c - '0';
		i++;
	    }

	    /* allow TZA before the year, so
	     * 'Wed Nov 05 21:49:11 GMT-0800 1997'
	     * works */

	    /* uses of seenplusminus allow : in TZA, so Java
	     * no-timezone style of GMT+4:30 works
	     */

	    if ((prevc == '+' || prevc == '-')/*  && year>=0 */) {
		/* make ':' case below change tzoffset */
		seenplusminus = JS_TRUE;

		/* offset */
		if (n < 24)
		    n = n * 60; /* EG. "GMT-3" */
		else
		    n = n % 100 + n / 100 * 60; /* eg "GMT-0430" */
		if (prevc == '+')       /* plus means east of GMT */
		    n = -n;
		if (tzoffset != 0 && tzoffset != -1)
		    goto syntax;
		tzoffset = n;
	    } else if (n >= 70 ||
		       (prevc == '/' && mon >= 0 && mday >= 0 && year < 0)) {
		if (year >= 0)
		    goto syntax;
		else if (c <= ' ' || c == ',' || c == '/' || i >= limit)
		    year = n < 100 ? n + 1900 : n;
		else
		    goto syntax;
	    } else if (c == ':') {
		if (hour < 0)
		    hour = /*byte*/ n;
		else if (min < 0)
		    min = /*byte*/ n;
		else
		    goto syntax;
	    } else if (c == '/') {
		if (mon < 0)
		    mon = /*byte*/ n-1;
		else if (mday < 0)
		    mday = /*byte*/ n;
		else
		    goto syntax;
	    } else if (i < limit && c != ',' && c > ' ' && c != '-') {
		goto syntax;
	    } else if (seenplusminus && n < 60) {  /* handle GMT-3:30 */
		if (tzoffset < 0)
		    tzoffset -= n;
		else
		    tzoffset += n;
	    } else if (hour >= 0 && min < 0) {
		min = /*byte*/ n;
	    } else if (min >= 0 && sec < 0) {
		sec = /*byte*/ n;
	    } else if (mday < 0) {
		mday = /*byte*/ n;
	    } else {
		goto syntax;
	    }
	    prevc = 0;
	} else if (c == '/' || c == ':' || c == '+' || c == '-') {
	    prevc = c;
	} else {
	    size_t st = i - 1;
	    int k;
	    while (i < limit) {
		c = s[i];
		if (!(('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')))
		    break;
		i++;
	    }
	    if (i <= st + 1)
		goto syntax;
	    for (k = (sizeof(wtb)/sizeof(char*)); --k >= 0;)
		if (date_regionMatches(wtb[k], 0, s, st, i-st, 1)) {
		    int action = ttb[k];
		    if (action != 0) {
                        if (action < 0) {
                            /*
                             * AM/PM. Count 12:30 AM as 00:30, 12:30 PM as
                             * 12:30, instead of blindly adding 12 if PM.
                             */
                            JS_ASSERT(action == -1 || action == -2);
                            if (hour > 12 || hour < 0) {
                                goto syntax;
                            } else {
                                if (action == -1 && hour == 12) { /* am */
                                    hour = 0;
                                } else if (action == -2 && hour != 12) { /* pm */
                                    hour += 12;
                                }
                            }
			} else if (action <= 13) { /* month! */
			    if (mon < 0) {
				mon = /*byte*/ (action - 2);
			    } else {
				goto syntax;
			    }
			} else {
			    tzoffset = action - 10000;
			}
		    }
		    break;
		}
	    if (k < 0)
		goto syntax;
	    prevc = 0;
	}
    }
    if (year < 0 || mon < 0 || mday < 0)
	goto syntax;
    if (sec < 0)
	sec = 0;
    if (min < 0)
	min = 0;
    if (hour < 0)
	hour = 0;
    if (tzoffset == -1) { /* no time zone specified, have to use local */
	jsdouble msec_time;
	msec_time = date_msecFromDate(year, mon, mday, hour, min, sec, 0);

	*result = UTC(msec_time);
	return JS_TRUE;
    }

    msec = date_msecFromDate(year, mon, mday, hour, min, sec, 0);
    msec += tzoffset * msPerMinute;
    *result = msec;
    return JS_TRUE;

syntax:
    /* syntax error */
    *result = 0;
    return JS_FALSE;
}

static JSBool
date_parse(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    JSString *str;
    jsdouble result;

    str = js_ValueToString(cx, argv[0]);
    if (!str)
	return JS_FALSE;
    if (!date_parseString(str, &result)) {
	*rval = DOUBLE_TO_JSVAL(cx->runtime->jsNaN);
	return JS_TRUE;
    }

    result = TIMECLIP(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_now(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    int64 us, ms, us2ms;
    jsdouble msec_time;

    us = PRMJ_Now();
    JSLL_UI2L(us2ms, PRMJ_USEC_PER_MSEC);
    JSLL_DIV(ms, us, us2ms);
    JSLL_L2D(msec_time, ms);

    return js_NewDoubleValue(cx, msec_time, rval);
}

/*
 * Check that obj is an object of class Date, and get the date value.
 * Return NULL on failure.
 */
static jsdouble *
date_getProlog(JSContext *cx, JSObject *obj, jsval *argv)
{
    if (!JS_InstanceOf(cx, obj, &date_class, argv))
	return NULL;
    return JSVAL_TO_DOUBLE(OBJ_GET_SLOT(cx, obj, JSSLOT_PRIVATE));
}

/*
 * See ECMA 15.9.5.4 thru 15.9.5.23
 */
static JSBool
date_getTime(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    return js_NewNumberValue(cx, *date, rval);
}

static JSBool
date_getYear(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = YearFromTime(LocalTime(result));

    /*
     * During the great date rewrite of 1.3, we tried to track the evolving ECMA
     * standard, which then had a definition of getYear which always subtracted
     * 1900.  Which we implemented, not realizing that it was incompatible with
     * the old behavior...  now, rather than thrash the behavior yet again,
     * we've decided to leave it with the - 1900 behavior and point people to
     * the getFullYear method.  But we try to protect existing scripts that
     * have specified a version...
     */
    if (cx->version == JSVERSION_1_0 ||
        cx->version == JSVERSION_1_1 ||
        cx->version == JSVERSION_1_2)
    {
        if (result >= 1900 && result < 2000)
            result -= 1900;
    } else {
        result -= 1900;
    }
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getFullYear(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		 jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = YearFromTime(LocalTime(result));
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getUTCFullYear(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		    jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = YearFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getMonth(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	      jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = MonthFromTime(LocalTime(result));
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getUTCMonth(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		 jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = MonthFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getDate(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = LocalTime(result);
    result = DateFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getUTCDate(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = DateFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getDay(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = LocalTime(result);
    result = WeekDay(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getUTCDay(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	       jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = WeekDay(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getHours(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	      jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = HourFromTime(LocalTime(result));
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getUTCHours(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		 jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = HourFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getMinutes(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = MinFromTime(LocalTime(result));
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getUTCMinutes(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		   jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = MinFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

/* Date.getSeconds is mapped to getUTCSeconds */

static JSBool
date_getUTCSeconds(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = SecFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

/* Date.getMilliseconds is mapped to getUTCMilliseconds */

static JSBool
date_getUTCMilliseconds(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		     jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    result = msFromTime(result);
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_getTimezoneOffset(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
		       jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    result = *date;

    /*
     * Return the time zone offset in minutes for the current locale
     * that is appropriate for this time. This value would be a
     * constant except for daylight savings time.
     */
    result = (result - LocalTime(result)) / msPerMinute;
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_setTime(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble result;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    if (!js_ValueToNumber(cx, argv[0], &result))
	return JS_FALSE;

    result = TIMECLIP(result);

    *date = result;
    return js_NewNumberValue(cx, result, rval);
}

static JSBool
date_makeTime(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	      uintN maxargs, JSBool local, jsval *rval)
{
    uintN i;
    jsdouble args[4], *argp, *stop;
    jsdouble hour, min, sec, msec;
    jsdouble lorutime; /* Local or UTC version of *date */

    jsdouble msec_time;
    jsdouble result;

    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    result = *date;

    /* just return NaN if the date is already NaN */
    if (!JSDOUBLE_IS_FINITE(result))
	return js_NewNumberValue(cx, result, rval);

    /* Satisfy the ECMA rule that if a function is called with
     * fewer arguments than the specified formal arguments, the
     * remaining arguments are set to undefined.  Seems like all
     * the Date.setWhatever functions in ECMA are only varargs
     * beyond the first argument; this should be set to undefined
     * if it's not given.  This means that "d = new Date();
     * d.setMilliseconds()" returns NaN.  Blech.
     */
    if (argc == 0)
	argc = 1;   /* should be safe, because length of all setters is 1 */
    else if (argc > maxargs)
	argc = maxargs;  /* clamp argc */

    for (i = 0; i < argc; i++) {
	if (!js_ValueToNumber(cx, argv[i], &args[i]))
	    return JS_FALSE;
	if (!JSDOUBLE_IS_FINITE(args[i])) {
	    *date = *cx->runtime->jsNaN;
	    return js_NewNumberValue(cx, *date, rval);
	}
	args[i] = js_DoubleToInteger(args[i]);
    }

    if (local)
	lorutime = LocalTime(result);
    else
	lorutime = result;

    argp = args;
    stop = argp + argc;
    if (maxargs >= 4 && argp < stop)
	hour = *argp++;
    else
	hour = HourFromTime(lorutime);

    if (maxargs >= 3 && argp < stop)
	min = *argp++;
    else
	min = MinFromTime(lorutime);

    if (maxargs >= 2 && argp < stop)
	sec = *argp++;
    else
	sec = SecFromTime(lorutime);

    if (maxargs >= 1 && argp < stop)
	msec = *argp;
    else
	msec = msFromTime(lorutime);

    msec_time = MakeTime(hour, min, sec, msec);
    result = MakeDate(Day(lorutime), msec_time);

/*     fprintf(stderr, "%f\n", result); */

    if (local)
	result = UTC(result);

/*     fprintf(stderr, "%f\n", result); */

    *date = TIMECLIP(result);
    return js_NewNumberValue(cx, *date, rval);
}

static JSBool
date_setMilliseconds(JSContext *cx, JSObject *obj, uintN argc,
		     jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 1, JS_TRUE, rval);
}

static JSBool
date_setUTCMilliseconds(JSContext *cx, JSObject *obj, uintN argc,
			jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 1, JS_FALSE, rval);
}

static JSBool
date_setSeconds(JSContext *cx, JSObject *obj, uintN argc,
		jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 2, JS_TRUE, rval);
}

static JSBool
date_setUTCSeconds(JSContext *cx, JSObject *obj, uintN argc,
		   jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 2, JS_FALSE, rval);
}

static JSBool
date_setMinutes(JSContext *cx, JSObject *obj, uintN argc,
		jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 3, JS_TRUE, rval);
}

static JSBool
date_setUTCMinutes(JSContext *cx, JSObject *obj, uintN argc,
		   jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 3, JS_FALSE, rval);
}

static JSBool
date_setHours(JSContext *cx, JSObject *obj, uintN argc,
	      jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 4, JS_TRUE, rval);
}

static JSBool
date_setUTCHours(JSContext *cx, JSObject *obj, uintN argc,
		 jsval *argv, jsval *rval)
{
    return date_makeTime(cx, obj, argc, argv, 4, JS_FALSE, rval);
}

static JSBool
date_makeDate(JSContext *cx, JSObject *obj, uintN argc,
	      jsval *argv, uintN maxargs, JSBool local, jsval *rval)
{
    uintN i;
    jsdouble lorutime; /* local or UTC version of *date */
    jsdouble args[3], *argp, *stop;
    jsdouble year, month, day;
    jsdouble result;

    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    result = *date;

    /* see complaint about ECMA in date_MakeTime */
    if (argc == 0)
	argc = 1;   /* should be safe, because length of all setters is 1 */
    else if (argc > maxargs)
	argc = maxargs;   /* clamp argc */

    for (i = 0; i < argc; i++) {
	if (!js_ValueToNumber(cx, argv[i], &args[i]))
	    return JS_FALSE;
	if (!JSDOUBLE_IS_FINITE(args[i])) {
	    *date = *cx->runtime->jsNaN;
	    return js_NewNumberValue(cx, *date, rval);
	}
	args[i] = js_DoubleToInteger(args[i]);
    }

    /* return NaN if date is NaN and we're not setting the year,
     * If we are, use 0 as the time. */
    if (!(JSDOUBLE_IS_FINITE(result))) {
	if (argc < 3)
	    return js_NewNumberValue(cx, result, rval);
	else
	    lorutime = +0.;
    } else {
	if (local)
	    lorutime = LocalTime(result);
	else
	    lorutime = result;
    }

    argp = args;
    stop = argp + argc;
    if (maxargs >= 3 && argp < stop)
	year = *argp++;
    else
	year = YearFromTime(lorutime);

    if (maxargs >= 2 && argp < stop)
	month = *argp++;
    else
	month = MonthFromTime(lorutime);

    if (maxargs >= 1 && argp < stop)
	day = *argp++;
    else
	day = DateFromTime(lorutime);

    day = MakeDay(year, month, day); /* day within year */
    result = MakeDate(day, TimeWithinDay(lorutime));

    if (local)
	result = UTC(result);

    *date = TIMECLIP(result);
    return js_NewNumberValue(cx, *date, rval);
}

static JSBool
date_setDate(JSContext *cx, JSObject *obj, uintN argc,
	     jsval *argv, jsval *rval)
{
    return date_makeDate(cx, obj, argc, argv, 1, JS_TRUE, rval);
}

static JSBool
date_setUTCDate(JSContext *cx, JSObject *obj, uintN argc,
		jsval *argv, jsval *rval)
{
    return date_makeDate(cx, obj, argc, argv, 1, JS_FALSE, rval);
}

static JSBool
date_setMonth(JSContext *cx, JSObject *obj, uintN argc,
	      jsval *argv, jsval *rval)
{
    return date_makeDate(cx, obj, argc, argv, 2, JS_TRUE, rval);
}

static JSBool
date_setUTCMonth(JSContext *cx, JSObject *obj, uintN argc,
		 jsval *argv, jsval *rval)
{
    return date_makeDate(cx, obj, argc, argv, 2, JS_FALSE, rval);
}

static JSBool
date_setFullYear(JSContext *cx, JSObject *obj, uintN argc,
		 jsval *argv, jsval *rval)
{
    return date_makeDate(cx, obj, argc, argv, 3, JS_TRUE, rval);
}

static JSBool
date_setUTCFullYear(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval)
{
    return date_makeDate(cx, obj, argc, argv, 3, JS_FALSE, rval);
}

static JSBool
date_setYear(JSContext *cx, JSObject *obj, uintN argc,
	     jsval *argv, jsval *rval)
{
    jsdouble t;
    jsdouble year;
    jsdouble day;
    jsdouble result;

    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    result = *date;

    if (!js_ValueToNumber(cx, argv[0], &year))
	return JS_FALSE;
    if (!JSDOUBLE_IS_FINITE(year)) {
	*date = *cx->runtime->jsNaN;
	return js_NewNumberValue(cx, *date, rval);
    }

    year = js_DoubleToInteger(year);

    if (!JSDOUBLE_IS_FINITE(result)) {
	t = +0.0;
    } else {
	t = LocalTime(result);
    }

    if (year >= 0 && year <= 99)
	year += 1900;

    day = MakeDay(year, MonthFromTime(t), DateFromTime(t));
    result = MakeDate(day, TimeWithinDay(t));
    result = UTC(result);

    *date = TIMECLIP(result);
    return js_NewNumberValue(cx, *date, rval);
}

/* constants for toString, toUTCString */
static char js_NaN_date_str[] = "Invalid Date";
static const char* days[] =
{
   "Sun","Mon","Tue","Wed","Thu","Fri","Sat"
};
static const char* months[] =
{
   "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

static JSBool
date_toGMTString(JSContext *cx, JSObject *obj, uintN argc,
		 jsval *argv, jsval *rval)
{
    char buf[100];
    JSString *str;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    if (!JSDOUBLE_IS_FINITE(*date)) {
	JS_snprintf(buf, sizeof buf, js_NaN_date_str);
    } else {
	jsdouble temp = *date;

	/* Avoid dependence on PRMJ_FormatTimeUSEnglish, because it
	 * requires a PRMJTime... which only has 16-bit years.  Sub-ECMA.
	 */
	JS_snprintf(buf, sizeof buf, "%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
		    days[WeekDay(temp)],
		    DateFromTime(temp),
		    months[MonthFromTime(temp)],
		    YearFromTime(temp),
		    HourFromTime(temp),
		    MinFromTime(temp),
		    SecFromTime(temp));
    }
    str = JS_NewStringCopyZ(cx, buf);
    if (!str)
	return JS_FALSE;
    *rval = STRING_TO_JSVAL(str);
    return JS_TRUE;
}

/* for Date.toLocaleString; interface to PRMJTime date struct.
 * If findEquivalent is true, then try to map the year to an equivalent year
 * that's in range.
 */
static void
new_explode(jsdouble timeval, PRMJTime *split, JSBool findEquivalent)
{
    jsint year = YearFromTime(timeval);
    int16 adjustedYear;

    /* If the year doesn't fit in a PRMJTime, find something to do about it. */
    if (year > 32767 || year < -32768) {
	if (findEquivalent) {
	    /* We're really just trying to get a timezone string; map the year
	     * to some equivalent year in the range 0 to 2800.  Borrowed from
	     * A. D. Olsen.
	     */
	    jsint cycles;
#define CYCLE_YEARS 2800L
	    cycles = (year >= 0) ? year / CYCLE_YEARS
				 : -1 - (-1 - year) / CYCLE_YEARS;
	    adjustedYear = (int16)(year - cycles * CYCLE_YEARS);
	} else {
	    /* Clamp it to the nearest representable year. */
	    adjustedYear = (int16)((year > 0) ? 32767 : - 32768);
	}
    } else {
	adjustedYear = (int16)year;
    }

    split->tm_usec = (int32) msFromTime(timeval) * 1000;
    split->tm_sec = (int8) SecFromTime(timeval);
    split->tm_min = (int8) MinFromTime(timeval);
    split->tm_hour = (int8) HourFromTime(timeval);
    split->tm_mday = (int8) DateFromTime(timeval);
    split->tm_mon = (int8) MonthFromTime(timeval);
    split->tm_wday = (int8) WeekDay(timeval);
    split->tm_year = (int16) adjustedYear;
    split->tm_yday = (int16) DayWithinYear(timeval, year);

    /* not sure how this affects things, but it doesn't seem
       to matter. */
    split->tm_isdst = (DaylightSavingTA(timeval) != 0);
}

typedef enum formatspec {
    FORMATSPEC_FULL, FORMATSPEC_DATE, FORMATSPEC_TIME
} formatspec;

/* helper function */
static JSBool
date_format(JSContext *cx, jsdouble date, formatspec format, jsval *rval)
{
    char buf[100];
    JSString *str;
    char tzbuf[100];
    JSBool usetz;
    size_t i, tzlen;
    PRMJTime split;

    if (!JSDOUBLE_IS_FINITE(date)) {
	JS_snprintf(buf, sizeof buf, js_NaN_date_str);
    } else {
	jsdouble local = LocalTime(date);

	/* offset from GMT in minutes.  The offset includes daylight savings,
	   if it applies. */
	jsint minutes = (jsint) floor(AdjustTime(date) / msPerMinute);

	/* map 510 minutes to 0830 hours */
	intN offset = (minutes / 60) * 100 + minutes % 60;

	/* print as "Wed Nov 05 19:38:03 GMT-0800 (PST) 1997" The TZA is
	 * printed as 'GMT-0800' rather than as 'PST' to avoid
	 * operating-system dependence on strftime (which
	 * PRMJ_FormatTimeUSEnglish calls, for %Z only.)  win32 prints
	 * PST as 'Pacific Standard Time.'  This way we always know
	 * what we're getting, and can parse it if we produce it.
	 * The OS TZA string is included as a comment.
	 */

	/* get a timezone string from the OS to include as a
	   comment. */
	new_explode(date, &split, JS_TRUE);
        if (PRMJ_FormatTime(tzbuf, sizeof tzbuf, "(%Z)", &split) != 0) {

            /* Decide whether to use the resulting timezone string.
             *
             * Reject it if it contains any non-ASCII, non-alphanumeric
             * characters.  It's then likely in some other character
             * encoding, and we probably won't display it correctly.
             */
            usetz = JS_TRUE;
            tzlen = strlen(tzbuf);
            if (tzlen > 100) {
                usetz = JS_FALSE;
            } else {
                for (i = 0; i < tzlen; i++) {
                    jschar c = tzbuf[i];
                    if (c > 127 ||
                        !(isalpha(c) || isdigit(c) ||
                          c == ' ' || c == '(' || c == ')')) {
                        usetz = JS_FALSE;
                    }
                }
            }

            /* Also reject it if it's not parenthesized or if it's '()'. */
            if (tzbuf[0] != '(' || tzbuf[1] == ')')
                usetz = JS_FALSE;
        } else
            usetz = JS_FALSE;

        switch (format) {
          case FORMATSPEC_FULL:
            /*
             * Avoid dependence on PRMJ_FormatTimeUSEnglish, because it
             * requires a PRMJTime... which only has 16-bit years.  Sub-ECMA.
             */
            /* Tue Oct 31 2000 09:41:40 GMT-0800 (PST) */
            JS_snprintf(buf, sizeof buf,
                        "%s %s %.2d %.4d %.2d:%.2d:%.2d GMT%+.4d%s%s",
                        days[WeekDay(local)],
                        months[MonthFromTime(local)],
                        DateFromTime(local),
                        YearFromTime(local),
                        HourFromTime(local),
                        MinFromTime(local),
                        SecFromTime(local),
                        offset,
                        usetz ? " " : "",
                        usetz ? tzbuf : "");
            break;
          case FORMATSPEC_DATE:
            /* Tue Oct 31 2000 */
            JS_snprintf(buf, sizeof buf,
                        "%s %s %.2d %.4d",
                        days[WeekDay(local)],
                        months[MonthFromTime(local)],
                        DateFromTime(local),
                        YearFromTime(local));
            break;
          case FORMATSPEC_TIME:
            /* 09:41:40 GMT-0800 (PST) */
            JS_snprintf(buf, sizeof buf,
                        "%.2d:%.2d:%.2d GMT%+.4d%s%s",
                        HourFromTime(local),
                        MinFromTime(local),
                        SecFromTime(local),
                        offset,
                        usetz ? " " : "",
                        usetz ? tzbuf : "");
            break;
        }
    }

    str = JS_NewStringCopyZ(cx, buf);
    if (!str)
	return JS_FALSE;
    *rval = STRING_TO_JSVAL(str);
    return JS_TRUE;
}

static JSBool
date_toLocaleHelper(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval, char *format)
{
    char buf[100];
    JSString *str;
    PRMJTime split;
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    if (!JSDOUBLE_IS_FINITE(*date)) {
	JS_snprintf(buf, sizeof buf, js_NaN_date_str);
    } else {
	intN result_len;
	jsdouble local = LocalTime(*date);
	new_explode(local, &split, JS_FALSE);

	/* let PRMJTime format it.	 */
	result_len = PRMJ_FormatTime(buf, sizeof buf, format, &split);

	/* If it failed, default to toString. */
	if (result_len == 0)
	    return date_format(cx, *date, FORMATSPEC_FULL, rval);

        /* Hacked check against undesired 2-digit year 00/00/00 form. */
        if (buf[result_len - 3] == '/' &&
            isdigit(buf[result_len - 2]) && isdigit(buf[result_len - 1])) {
            JS_snprintf(buf + (result_len - 2), (sizeof buf) - (result_len - 2),
                        "%d", js_DateGetYear(cx, obj));
        }

    }

    if (cx->localeCallbacks && cx->localeCallbacks->localeToUnicode)
	return cx->localeCallbacks->localeToUnicode(cx, buf, rval);
    
    str = JS_NewStringCopyZ(cx, buf);
    if (!str)
	return JS_FALSE;
    *rval = STRING_TO_JSVAL(str);
    return JS_TRUE;
}

static JSBool
date_toLocaleString(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval)
{
    /* Use '%#c' for windows, because '%c' is
     * backward-compatible and non-y2k with msvc; '%#c' requests that a
     * full year be used in the result string.
     */
    return date_toLocaleHelper(cx, obj, argc, argv, rval,
#if defined(_WIN32) && !defined(__MWERKS__)
				   "%#c"
#else
				   "%c"
#endif
				   );
}

static JSBool
date_toLocaleDateString(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval)
{
    /* Use '%#x' for windows, because '%x' is
     * backward-compatible and non-y2k with msvc; '%#x' requests that a
     * full year be used in the result string.
     */
    return date_toLocaleHelper(cx, obj, argc, argv, rval,
#if defined(_WIN32) && !defined(__MWERKS__)
				   "%#x"
#else
				   "%x"
#endif
				   );
}

static JSBool
date_toLocaleTimeString(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval)
{
    return date_toLocaleHelper(cx, obj, argc, argv, rval, "%X");
}

static JSBool
date_toTimeString(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval)
{
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    return date_format(cx, *date, FORMATSPEC_TIME, rval);
}

static JSBool
date_toDateString(JSContext *cx, JSObject *obj, uintN argc,
		    jsval *argv, jsval *rval)
{
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    return date_format(cx, *date, FORMATSPEC_DATE, rval);
}

#if JS_HAS_TOSOURCE
#include <string.h>
#include "jsdtoa.h"

static JSBool
date_toSource(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	      jsval *rval)
{
    jsdouble *date;
    char buf[DTOSTR_STANDARD_BUFFER_SIZE], *numStr, *bytes;
    JSString *str;

    date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;

    numStr = JS_dtostr(buf, sizeof buf, DTOSTR_STANDARD, 0, *date);
    if (!numStr) {
	JS_ReportOutOfMemory(cx);
	return JS_FALSE;
    }

    bytes = JS_smprintf("(new %s(%s))", date_class.name, numStr);
    if (!bytes) {
	JS_ReportOutOfMemory(cx);
	return JS_FALSE;
    }

    str = JS_NewString(cx, bytes, strlen(bytes));
    if (!str) {
	free(bytes);
	return JS_FALSE;
    }
    *rval = STRING_TO_JSVAL(str);
    return JS_TRUE;
}
#endif

static JSBool
date_toString(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	      jsval *rval)
{
    jsdouble *date = date_getProlog(cx, obj, argv);
    if (!date)
	return JS_FALSE;
    return date_format(cx, *date, FORMATSPEC_FULL, rval);
}

#if JS_HAS_VALUEOF_HINT
static JSBool
date_valueOf(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
	     jsval *rval)
{
    /* It is an error to call date_valueOf on a non-date object, but we don't
     * need to check for that explicitly here because every path calls
     * date_getProlog, which does the check.
     */

    /* If called directly with no arguments, convert to a time number. */
    if (argc == 0)
	return date_getTime(cx, obj, argc, argv, rval);

    /* Convert to number only if the hint was given, otherwise favor string. */
    if (argc == 1) {
	JSString *str, *str2;

	str = js_ValueToString(cx, argv[0]);
	if (!str)
	    return JS_FALSE;
	str2 = ATOM_TO_STRING(cx->runtime->atomState.typeAtoms[JSTYPE_NUMBER]);
	if (!js_CompareStrings(str, str2))
	    return date_getTime(cx, obj, argc, argv, rval);
    }
    return date_toString(cx, obj, argc, argv, rval);
}
#else
#define date_valueOf date_getTime
#endif


/*
 * creation and destruction
 */

static JSFunctionSpec date_static_methods[] = {
    {"UTC",               date_UTC,               MAXARGS,0,0 },
    {"parse",             date_parse,             1,0,0 },
    {"now",               date_now,               0,0,0 },
    {0,0,0,0,0}
};

static JSFunctionSpec date_methods[] = {
    {"getTime",             date_getTime,           0,0,0 },
    {"getTimezoneOffset",   date_getTimezoneOffset, 0,0,0 },
    {"getYear",             date_getYear,           0,0,0 },
    {"getFullYear",         date_getFullYear,       0,0,0 },
    {"getUTCFullYear",      date_getUTCFullYear,    0,0,0 },
    {"getMonth",            date_getMonth,          0,0,0 },
    {"getUTCMonth",         date_getUTCMonth,       0,0,0 },
    {"getDate",             date_getDate,           0,0,0 },
    {"getUTCDate",          date_getUTCDate,        0,0,0 },
    {"getDay",              date_getDay,            0,0,0 },
    {"getUTCDay",           date_getUTCDay,         0,0,0 },
    {"getHours",            date_getHours,          0,0,0 },
    {"getUTCHours",         date_getUTCHours,       0,0,0 },
    {"getMinutes",          date_getMinutes,        0,0,0 },
    {"getUTCMinutes",       date_getUTCMinutes,     0,0,0 },
    {"getSeconds",          date_getUTCSeconds,     0,0,0 },
    {"getUTCSeconds",       date_getUTCSeconds,     0,0,0 },
    {"getMilliseconds",     date_getUTCMilliseconds,0,0,0 },
    {"getUTCMilliseconds",  date_getUTCMilliseconds,0,0,0 },
    {"setTime",             date_setTime,           1,0,0 },
    {"setYear",             date_setYear,           1,0,0 },
    {"setFullYear",         date_setFullYear,       3,0,0 },
    {"setUTCFullYear",      date_setUTCFullYear,    3,0,0 },
    {"setMonth",            date_setMonth,          2,0,0 },
    {"setUTCMonth",         date_setUTCMonth,       2,0,0 },
    {"setDate",             date_setDate,           1,0,0 },
    {"setUTCDate",          date_setUTCDate,        1,0,0 },
    {"setHours",            date_setHours,          4,0,0 },
    {"setUTCHours",         date_setUTCHours,       4,0,0 },
    {"setMinutes",          date_setMinutes,        3,0,0 },
    {"setUTCMinutes",       date_setUTCMinutes,     3,0,0 },
    {"setSeconds",          date_setSeconds,        2,0,0 },
    {"setUTCSeconds",       date_setUTCSeconds,     2,0,0 },
    {"setMilliseconds",     date_setMilliseconds,   1,0,0 },
    {"setUTCMilliseconds",  date_setUTCMilliseconds,1,0,0 },
    {"toUTCString",         date_toGMTString,       0,0,0 },
    {js_toLocaleString_str, date_toLocaleString,    0,0,0 },
    {"toLocaleDateString",  date_toLocaleDateString,0,0,0 },
    {"toLocaleTimeString",  date_toLocaleTimeString,0,0,0 },
    {"toDateString",        date_toDateString,      0,0,0 },
    {"toTimeString",        date_toTimeString,      0,0,0 },
#if JS_HAS_TOSOURCE
    {js_toSource_str,       date_toSource,          0,0,0 },
#endif
    {js_toString_str,       date_toString,          0,0,0 },
    {js_valueOf_str,        date_valueOf,           0,0,0 },
    {0,0,0,0,0}
};

static jsdouble *
date_constructor(JSContext *cx, JSObject* obj)
{
    jsdouble *date;

    date = js_NewDouble(cx, 0.0);
    if (!date)
	return NULL;
    OBJ_SET_SLOT(cx, obj, JSSLOT_PRIVATE, DOUBLE_TO_JSVAL(date));
    return date;
}

static JSBool
Date(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble *date;
    JSString *str;
    jsdouble d;

    /* Date called as function */
    if (!(cx->fp->flags & JSFRAME_CONSTRUCTING)) {
	int64 us, ms, us2ms;
	jsdouble msec_time;

	/* NSPR 2.0 docs say 'We do not support PRMJ_NowMS and PRMJ_NowS',
	 * so compute ms from PRMJ_Now.
	 */
	us = PRMJ_Now();
	JSLL_UI2L(us2ms, PRMJ_USEC_PER_MSEC);
	JSLL_DIV(ms, us, us2ms);
	JSLL_L2D(msec_time, ms);

	return date_format(cx, msec_time, FORMATSPEC_FULL, rval);
    }

    /* Date called as constructor */
    if (argc == 0) {
	int64 us, ms, us2ms;
	jsdouble msec_time;

	date = date_constructor(cx, obj);
	if (!date)
	    return JS_FALSE;

	us = PRMJ_Now();
	JSLL_UI2L(us2ms, PRMJ_USEC_PER_MSEC);
	JSLL_DIV(ms, us, us2ms);
	JSLL_L2D(msec_time, ms);

	*date = msec_time;
    } else if (argc == 1) {
	if (!JSVAL_IS_STRING(argv[0])) {
	    /* the argument is a millisecond number */
	    if (!js_ValueToNumber(cx, argv[0], &d))
		    return JS_FALSE;
	    date = date_constructor(cx, obj);
	    if (!date)
		return JS_FALSE;
	    *date = TIMECLIP(d);
	} else {
	    /* the argument is a string; parse it. */
	    date = date_constructor(cx, obj);
	    if (!date)
		return JS_FALSE;

	    str = js_ValueToString(cx, argv[0]);
	    if (!str)
		return JS_FALSE;

	    if (!date_parseString(str, date))
		*date = *cx->runtime->jsNaN;
	    *date = TIMECLIP(*date);
	}
    } else {
	jsdouble array[MAXARGS];
	uintN loop;
	jsdouble double_arg;
	jsdouble day;
	jsdouble msec_time;

	for (loop = 0; loop < MAXARGS; loop++) {
	    if (loop < argc) {
		if (!js_ValueToNumber(cx, argv[loop], &double_arg))
		    return JS_FALSE;
		/* if any arg is NaN, make a NaN date object
		   and return */
		if (!JSDOUBLE_IS_FINITE(double_arg)) {
		    date = date_constructor(cx, obj);
		    if (!date)
			return JS_FALSE;
		    *date = *cx->runtime->jsNaN;
		    return JS_TRUE;
		}
		array[loop] = js_DoubleToInteger(double_arg);
	    } else {
                if (loop == 2) {
                    array[loop] = 1; /* Default the date argument to 1. */
                } else {
                    array[loop] = 0;
                }
	    }
	}

	date = date_constructor(cx, obj);
	if (!date)
	    return JS_FALSE;

	/* adjust 2-digit years into the 20th century */
	if (array[0] >= 0 && array[0] <= 99)
	    array[0] += 1900;

	day = MakeDay(array[0], array[1], array[2]);
	msec_time = MakeTime(array[3], array[4], array[5], array[6]);
	msec_time = MakeDate(day, msec_time);
	msec_time = UTC(msec_time);
	*date = TIMECLIP(msec_time);
    }
    return JS_TRUE;
}

JSObject *
js_InitDateClass(JSContext *cx, JSObject *obj)
{
    JSObject *proto;
    jsdouble *proto_date;

    /* set static LocalTZA */
    LocalTZA = -(PRMJ_LocalGMTDifference() * msPerSecond);
    proto = JS_InitClass(cx, obj, NULL, &date_class, Date, MAXARGS,
			 NULL, date_methods, NULL, date_static_methods);
    if (!proto)
	return NULL;

    /* Alias toUTCString with toGMTString.  (ECMA B.2.6) */
    if (!JS_AliasProperty(cx, proto, "toUTCString", "toGMTString"))
        return NULL;

    /* Set the value of the Date.prototype date to NaN */
    proto_date = date_constructor(cx, proto);
    if (!proto_date)
	return NULL;
    *proto_date = *cx->runtime->jsNaN;

    return proto;
}

JS_FRIEND_API(JSObject *)
js_NewDateObjectMsec(JSContext *cx, jsdouble msec_time)
{
    JSObject *obj;
    jsdouble *date;

    obj = js_NewObject(cx, &date_class, NULL, NULL);
    if (!obj)
	return NULL;

    date = date_constructor(cx, obj);
    if (!date)
	return NULL;

    *date = msec_time;
    return obj;
}

JS_FRIEND_API(JSObject *)
js_NewDateObject(JSContext* cx, int year, int mon, int mday,
                 int hour, int min, int sec)
{
    JSObject *obj;
    jsdouble msec_time;

    msec_time = date_msecFromDate(year, mon, mday, hour, min, sec, 0);
    obj = js_NewDateObjectMsec(cx, UTC(msec_time));
    return obj;
}

JS_FRIEND_API(JSBool)
js_DateIsValid(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    if (!date || JSDOUBLE_IS_NaN(*date))
        return JS_FALSE;
    else
        return JS_TRUE;
}

JS_FRIEND_API(int)
js_DateGetYear(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    /* Preserve legacy API behavior of returning 0 for invalid dates. */
    if (!date || JSDOUBLE_IS_NaN(*date))
	return 0;
    return (int) YearFromTime(LocalTime(*date));
}

JS_FRIEND_API(int)
js_DateGetMonth(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    if (!date || JSDOUBLE_IS_NaN(*date))
	return 0;
    return (int) MonthFromTime(LocalTime(*date));
}

JS_FRIEND_API(int)
js_DateGetDate(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    if (!date || JSDOUBLE_IS_NaN(*date))
	return 0;
    return (int) DateFromTime(LocalTime(*date));
}

JS_FRIEND_API(int)
js_DateGetHours(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    if (!date || JSDOUBLE_IS_NaN(*date))
	return 0;
    return (int) HourFromTime(LocalTime(*date));
}

JS_FRIEND_API(int)
js_DateGetMinutes(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    if (!date || JSDOUBLE_IS_NaN(*date))
	return 0;
    return (int) MinFromTime(LocalTime(*date));
}

JS_FRIEND_API(int)
js_DateGetSeconds(JSContext *cx, JSObject* obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);

    if (!date || JSDOUBLE_IS_NaN(*date))
	return 0;
    return (int) SecFromTime(*date);
}

JS_FRIEND_API(void)
js_DateSetYear(JSContext *cx, JSObject *obj, int year)
{
    jsdouble local;
    jsdouble *date = date_getProlog(cx, obj, NULL);
    if (!date)
	return;
    local = LocalTime(*date);
    /* reset date if it was NaN */
    if (JSDOUBLE_IS_NaN(local))
	local = 0;
    local = date_msecFromDate(year,
			      MonthFromTime(local),
			      DateFromTime(local),
			      HourFromTime(local),
			      MinFromTime(local),
			      SecFromTime(local),
			      msFromTime(local));
    *date = UTC(local);
}

JS_FRIEND_API(void)
js_DateSetMonth(JSContext *cx, JSObject *obj, int month)
{
    jsdouble local;
    jsdouble *date = date_getProlog(cx, obj, NULL);
    if (!date)
	return;
    local = LocalTime(*date);
    /* bail if date was NaN */
    if (JSDOUBLE_IS_NaN(local))
	return;
    local = date_msecFromDate(YearFromTime(local),
			      month,
			      DateFromTime(local),
			      HourFromTime(local),
			      MinFromTime(local),
			      SecFromTime(local),
			      msFromTime(local));
    *date = UTC(local);
}

JS_FRIEND_API(void)
js_DateSetDate(JSContext *cx, JSObject *obj, int date)
{
    jsdouble local;
    jsdouble *datep = date_getProlog(cx, obj, NULL);
    if (!datep)
	return;
    local = LocalTime(*datep);
    if (JSDOUBLE_IS_NaN(local))
	return;
    local = date_msecFromDate(YearFromTime(local),
			      MonthFromTime(local),
			      date,
			      HourFromTime(local),
			      MinFromTime(local),
			      SecFromTime(local),
			      msFromTime(local));
    *datep = UTC(local);
}

JS_FRIEND_API(void)
js_DateSetHours(JSContext *cx, JSObject *obj, int hours)
{
    jsdouble local;
    jsdouble *date = date_getProlog(cx, obj, NULL);
    if (!date)
	return;
    local = LocalTime(*date);
    if (JSDOUBLE_IS_NaN(local))
	return;
    local = date_msecFromDate(YearFromTime(local),
			      MonthFromTime(local),
			      DateFromTime(local),
			      hours,
			      MinFromTime(local),
			      SecFromTime(local),
			      msFromTime(local));
    *date = UTC(local);
}

JS_FRIEND_API(void)
js_DateSetMinutes(JSContext *cx, JSObject *obj, int minutes)
{
    jsdouble local;
    jsdouble *date = date_getProlog(cx, obj, NULL);
    if (!date)
	return;
    local = LocalTime(*date);
    if (JSDOUBLE_IS_NaN(local))
	return;
    local = date_msecFromDate(YearFromTime(local),
			      MonthFromTime(local),
			      DateFromTime(local),
			      HourFromTime(local),
			      minutes,
			      SecFromTime(local),
			      msFromTime(local));
    *date = UTC(local);
}

JS_FRIEND_API(void)
js_DateSetSeconds(JSContext *cx, JSObject *obj, int seconds)
{
    jsdouble local;
    jsdouble *date = date_getProlog(cx, obj, NULL);
    if (!date)
	return;
    local = LocalTime(*date);
    if (JSDOUBLE_IS_NaN(local))
	return;
    local = date_msecFromDate(YearFromTime(local),
			      MonthFromTime(local),
			      DateFromTime(local),
			      HourFromTime(local),
			      MinFromTime(local),
			      seconds,
			      msFromTime(local));
    *date = UTC(local);
}

JS_FRIEND_API(jsdouble)
js_DateGetMsecSinceEpoch(JSContext *cx, JSObject *obj)
{
    jsdouble *date = date_getProlog(cx, obj, NULL);
    if (!date || JSDOUBLE_IS_NaN(*date))
        return 0;
    return (*date);
}
