
/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) NGINX, Inc.
 */


#include <njs_main.h>


#define NJS_DATE_TIME_LEN                                                     \
    sizeof("Mon Sep 28 1970 12:00:00 GMT+0600 (XXXXX)")

#define NJS_DATE_MAX_FIELDS             8
#define NJS_DATE_WDAY                   0
#define NJS_DATE_YR                     1
#define NJS_DATE_MON                    2
#define NJS_DATE_DAY                    3
#define NJS_DATE_HR                     4
#define NJS_DATE_MIN                    5
#define NJS_DATE_SEC                    6
#define NJS_DATE_MSEC                   7


#if (NJS_HAVE_TM_GMTOFF)

#define njs_timezone(tm)                                                      \
    ((tm)->tm_gmtoff)

#elif (NJS_HAVE_ALTZONE)

#define njs_timezone(tm)                                                      \
    (-(((tm)->tm_isdst > 0) ? altzone : timezone))

#endif


#define njs_date_magic(field, local)                                          \
    ((local << 6) + field)


#define njs_date_magic2(since, len, local)                                    \
    ((local << 6) + ((len & 7) << 3) + since)


typedef enum {
    NJS_DATE_FMT_TO_TIME_STRING,
    NJS_DATE_FMT_TO_DATE_STRING,
    NJS_DATE_FMT_TO_STRING,
    NJS_DATE_FMT_TO_UTC_STRING,
    NJS_DATE_FMT_TO_ISO_STRING,
} njs_date_fmt_t;


static double njs_date_string_parse(njs_vm_t *vm, njs_value_t *date);
static double njs_date_rfc2822_string_parse(int64_t tm[], const u_char *p,
    const u_char *end);
static double njs_date_js_string_parse(int64_t tm[], const u_char *p,
    const u_char *end);
static const u_char *njs_date_skip_week_day(const u_char *p, const u_char *end);
static const u_char *njs_date_skip_spaces(const u_char *p, const u_char *end);
static njs_int_t njs_date_month_parse(const u_char *p, const u_char *end);
static const u_char *njs_date_time_parse(int64_t tm[], const u_char *p,
    const u_char *end);
static int64_t njs_date_gmtoff_parse(const u_char *start, const u_char *end);
static const u_char *njs_date_number_parse(int64_t *value, const u_char *p,
    const u_char *end, size_t size);
static njs_int_t njs_date_string(njs_vm_t *vm, njs_value_t *retval,
    njs_date_fmt_t fmt, double time);


njs_inline int64_t
njs_mod(int64_t a, int64_t b)
{
    int64_t  m;

    m = a % b;

    return m + (m < 0) * b;
}


njs_inline int64_t
njs_floor_div(int64_t a, int64_t b)
{
    int64_t  m;

    m = a % b;

    return (a - (m + (m < 0) * b)) / b;
}


njs_inline uint64_t
njs_gettime(void)
{
    struct timeval  tv;

    gettimeofday(&tv, NULL);

    return (uint64_t) tv.tv_sec * 1000 + tv.tv_usec / 1000;
}


njs_inline double
njs_timeclip(double time)
{
    if (time < -8.64e15 || time > 8.64e15) {
        return NAN;
    }

    return trunc(time) + 0.0;
}


njs_inline int64_t
njs_days_in_year(int64_t y)
{
    return 365 + !(y % 4) - !(y % 100) + !(y % 400);
}


njs_inline int64_t
njs_days_from_year(int64_t y)
{
    return 365 * (y - 1970) + njs_floor_div(y - 1969, 4)
           - njs_floor_div(y - 1901, 100) + njs_floor_div(y - 1601, 400);
}


njs_inline double
njs_make_day(int64_t yr, int64_t month, int64_t date)
{
    double   days;
    int64_t  i, ym, mn, md;

    static const int min_year = -271821;
    static const int max_year = 275760;
    static const int month_days[] = { 31, 28, 31, 30, 31, 30,
                                      31, 31, 30, 31, 30, 31 };

    if (yr < min_year || yr > max_year
        || month < (min_year * 12) || month > (max_year * 12)
        || date < (min_year * 12 * 366) || date > (max_year * 12 * 366))
    {
        return NAN;
    }

    mn = njs_mod(month, 12);
    ym = yr + (month - mn) / 12;

    days = njs_days_from_year(ym);

    for (i = 0; i < mn; i++) {
        md = month_days[i];

        if (i == 1) {
            /* Leap day. */
            md += njs_days_in_year(ym) - 365;
        }

        days += md;
    }

    return days + date - 1;
}


njs_inline int64_t
njs_tz_offset(int64_t time)
{
    time_t     ti;
    struct tm  tm;

    time /= 1000;

#if (NJS_TIME_T_SIZE < 8)

    /* Smart truncation. */

    if ((time_t) -1 < 0) {
        if (time < INT32_MIN) {
            time = INT32_MIN;

        } else if (time > INT32_MAX) {
            time = INT32_MAX;
        }

    } else {
        if (time < 0) {
            time = 0;

        } else if (time > UINT32_MAX) {
            time = UINT32_MAX;
        }
    }

#endif

    ti = time;
    localtime_r(&ti, &tm);

    /*
     * As njs_timezone(&tm) may return value which is not a multiple of 60
     * secs (see "zdump -v /etc/localtime" for MSK zone) rounding it to
     * minutes precision here to ensure:
     * var date = new Date(<args>)
     * date.valueOf() - date.getTimezoneOffset() * 60000 == Date.UTC(<args>)
     * which is expected by test262.
     */

    return -njs_timezone(&tm) / 60;
}


njs_inline int64_t
njs_year_from_days(int64_t *days)
{
    int64_t  y, d1, nd, d;

    d = *days;

    y = njs_floor_div(d * 10000, 3652425) + 1970;

    for ( ;; ) {
        d1 = d - njs_days_from_year(y);

        if (d1 < 0) {
            y--;

        } else {
            nd = njs_days_in_year(y);

            if (d1 < nd) {
                break;
            }

            y++;
        }
    }

    *days = d1;

    return y;
}


njs_inline double
njs_make_date(int64_t tm[], njs_bool_t local)
{
    double  time, days;

    days = njs_make_day(tm[NJS_DATE_YR], tm[NJS_DATE_MON],
                        tm[NJS_DATE_DAY]);

    if (njs_slow_path(isnan(days))) {
        return NAN;
    }

    time = ((tm[NJS_DATE_HR] * 60.0 + tm[NJS_DATE_MIN]) * 60.0
            + tm[NJS_DATE_SEC]) * 1000.0 + tm[NJS_DATE_MSEC];

    time += days * 86400000.0;

    if (time < -8.64e15 || time > 8.64e15) {
        return NAN;
    }

    if (local) {
        time += njs_tz_offset(time) * 60000;
    }

    return njs_timeclip(time);
}


njs_inline int64_t
njs_destruct_date(double time, int64_t tm[], int index, njs_bool_t local)
{
    int64_t  days, wd, y, i, md, h, m, s, ms;

    static const int month_days[] = { 31, 28, 31, 30, 31, 30,
                                      31, 31, 30, 31, 30, 31 };

    if (njs_slow_path(isnan(time))) {
        time = 0;

    } else if (local) {
        time -= njs_tz_offset(time) * 60000;
    }

    h = njs_mod(time, 86400000);
    days = (time - h) / 86400000;
    ms = h % 1000;
    h = (h - ms) / 1000;
    s = h % 60;
    h = (h - s) / 60;
    m = h % 60;
    h = (h - m) / 60;
    wd = njs_mod(days + 4, 7);
    y = njs_year_from_days(&days);

    for (i = 0; i < 11; i++) {
        md = month_days[i];

        if (i == 1) {
            /* Leap day. */
            md += njs_days_in_year(y) - 365;
        }

        if (days < md) {
            break;
        }

        days -= md;
    }

    tm[NJS_DATE_YR] = y;
    tm[NJS_DATE_MON] = i;
    tm[NJS_DATE_DAY] = days + 1;
    tm[NJS_DATE_HR] = h;
    tm[NJS_DATE_MIN] = m;
    tm[NJS_DATE_SEC] = s;
    tm[NJS_DATE_MSEC] = ms;
    tm[NJS_DATE_WDAY] = wd;

    return tm[index];
}


static njs_int_t
njs_date_args(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    int64_t tm[])
{
    double      num;
    njs_int_t   ret;
    njs_uint_t  i, n;

    njs_memzero(tm, NJS_DATE_MAX_FIELDS * sizeof(int64_t));

    tm[NJS_DATE_DAY] = 1;

    n = njs_min(8, nargs);

    for (i = 1; i < n; i++) {
        ret = njs_value_to_number(vm, &args[i], &num);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        if (!isfinite(num)) {
            tm[NJS_DATE_YR] = INT64_MIN;
            continue;
        }

        tm[i] = njs_number_to_integer(num);
    }

    if (tm[NJS_DATE_YR] >= 0 && tm[NJS_DATE_YR] < 100) {
        tm[NJS_DATE_YR] += 1900;
    }

    return NJS_OK;
}


njs_date_t *
njs_date_alloc(njs_vm_t *vm, double time)
{
    njs_date_t  *date;

    date = njs_mp_alloc(vm->mem_pool, sizeof(njs_date_t));
    if (njs_slow_path(date == NULL)) {
        njs_memory_error(vm);
        return NULL;
    }

    njs_flathsh_init(&date->object.hash);
    njs_flathsh_init(&date->object.shared_hash);
    date->object.type = NJS_DATE;
    date->object.shared = 0;
    date->object.extensible = 1;
    date->object.error_data = 0;
    date->object.fast_array = 0;
    date->object.__proto__ = njs_vm_proto(vm, NJS_OBJ_TYPE_DATE);
    date->object.slots = NULL;

    date->time = time;

    return date;
}


static njs_int_t
njs_date_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    double      time;
    njs_int_t   ret;
    njs_date_t  *date;
    int64_t     tm[NJS_DATE_MAX_FIELDS];

    if (!vm->top_frame->ctor) {
        return njs_date_string(vm, retval, NJS_DATE_FMT_TO_STRING,
                               njs_gettime());
    }

    if (nargs == 1) {
        time = njs_gettime();

    } else if (nargs == 2) {
        if (njs_is_object(&args[1])) {
            if (!njs_is_date(&args[1])) {
                ret = njs_value_to_primitive(vm, &args[1], &args[1],
                                             NJS_HINT_NONE);
                if (njs_slow_path(ret != NJS_OK)) {
                    return ret;
                }
            }
        }

        if (njs_is_date(&args[1])) {
            time = njs_date(&args[1])->time;

        } else if (njs_is_string(&args[1])) {
            time = njs_date_string_parse(vm, &args[1]);

        } else {
            time = njs_timeclip(njs_number(&args[1]));
        }

    } else {

        ret = njs_date_args(vm, args, nargs, tm);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        time = njs_make_date(tm, 1);
    }

    date = njs_date_alloc(vm, time);
    if (njs_slow_path(date == NULL)) {
        return NJS_ERROR;
    }

    njs_set_date(retval, date);

    return NJS_OK;
}


static njs_int_t
njs_date_utc(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    double      time;
    njs_int_t   ret;
    int64_t     tm[NJS_DATE_MAX_FIELDS];

    time = NAN;

    if (nargs > 1) {
        ret = njs_date_args(vm, args, nargs, tm);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        time = njs_make_date(tm, 0);
    }

    njs_set_number(retval, time);

    return NJS_OK;
}


static njs_int_t
njs_date_now(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_set_number(retval, njs_gettime());

    return NJS_OK;
}


static njs_int_t
njs_date_parse(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    double     time;
    njs_int_t  ret;

    if (nargs > 1) {
        if (njs_slow_path(!njs_is_string(&args[1]))) {
            ret = njs_value_to_string(vm, &args[1], &args[1]);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }
        }

        time = njs_date_string_parse(vm, &args[1]);

    } else {
        time = NAN;
    }

    njs_set_number(retval, time);

    return NJS_OK;
}


static int64_t
njs_date_utc_offset_parse(const u_char *start, const u_char *end)
{
    int64_t       utc_off, hour, min;
    const u_char  *p;

    if (njs_fast_path(start + 2 < end && (*start == '+' || *start == '-'))) {
        p = njs_date_number_parse(&hour, start + 1, end, 2);
        if (njs_slow_path(p == NULL || hour > 23)) {
            return -1;
        }

        if (p < end && *p == ':') {
            p++;
        }

        p = njs_date_number_parse(&min, p, end, 2);
        if (njs_slow_path(p == NULL || min > 59)) {
            return -1;
        }

        utc_off = hour * 60 + min;

        if (*start == '-') {
            utc_off = -utc_off;
        }

        return utc_off;
    }

    return -1;
}

static double
njs_date_string_parse(njs_vm_t *vm, njs_value_t *date)
{
    size_t         ms_length;
    int64_t        ext, utc_off;
    njs_str_t      string;
    njs_bool_t     sign, week, utc;
    const u_char   *p, *next, *end;
    int64_t        tm[NJS_DATE_MAX_FIELDS];

    njs_string_get(vm, date, &string);

    p = string.start;
    end = p + string.length;

    if (njs_slow_path(p >= end)) {
        return NAN;
    }

    if (*p == '+' || *p == '-') {
        p++;
        sign = 1;

    } else {
        sign = 0;
    }

    tm[NJS_DATE_MON] = 0;
    tm[NJS_DATE_DAY] = 1;
    tm[NJS_DATE_HR] = 0;
    tm[NJS_DATE_MIN] = 0;
    tm[NJS_DATE_SEC] = 0;
    tm[NJS_DATE_MSEC] = 0;

    next = njs_date_number_parse(&tm[NJS_DATE_YR], p, end, 4);

    if (next != NULL) {
        utc = 1;

        /* ISO-8601 format: "1970-09-28T06:00:00.000Z" */

        if (next == end) {
            goto done;
        }

        if (*next != '-') {
            /* Extended ISO-8601 format: "+001970-09-28T06:00:00.000Z" */

            next = njs_date_number_parse(&ext, next, end, 2);
            if (njs_slow_path(next == NULL)) {
                return NAN;
            }

            tm[NJS_DATE_YR] *= 100;
            tm[NJS_DATE_YR] += ext;

            if (string.start[0] == '-') {
                if (tm[NJS_DATE_YR] == 0) {
                    return NAN;
                }

                tm[NJS_DATE_YR] = -tm[NJS_DATE_YR];
            }

            if (next == end) {
                goto done;
            }

            if (*next != '-') {
                return NAN;
            }
        }

        p = njs_date_number_parse(&tm[NJS_DATE_MON], next + 1, end, 2);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        tm[NJS_DATE_MON]--;

        if (p == end) {
            goto done;
        }

        if (njs_slow_path(*p != '-')) {
            return NAN;
        }

        p = njs_date_number_parse(&tm[NJS_DATE_DAY], p + 1, end, 2);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        if (p == end) {
            goto done;
        }

        if (njs_slow_path(*p != 'T')) {
            return NAN;
        }

        end--;

        if (*end != 'Z') {
           utc = 0;
           end++;
        }

        p = njs_date_time_parse(tm, p + 1, end);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        if (p == end) {
            goto done;
        }

        if (njs_slow_path(p > end || *p != '.')) {
            return NAN;
        }

        p++;

        for (ms_length = 0; p + ms_length < end; ms_length++) {
            if (p[ms_length] < '0' || p[ms_length] > '9') {
                break;
            }
        }

        if (njs_slow_path(njs_date_number_parse(&tm[NJS_DATE_MSEC], p, end,
                                                njs_min(ms_length, 3))
                          == NULL))
        {
            return NAN;
        }

        if (ms_length == 1) {
            tm[NJS_DATE_MSEC] *= 100;

        } else if (ms_length == 2) {
            tm[NJS_DATE_MSEC] *= 10;
        }

        p += ms_length;

        if (p < end) {
            utc_off = njs_date_utc_offset_parse(p, end);
            if (njs_slow_path(utc_off == -1)) {
                return NAN;
            }

            utc = 1;
            tm[NJS_DATE_MSEC] += -utc_off * 60000;
        }

done:

        return njs_make_date(tm, !utc);
    }

    if (sign) {
        return NAN;
    }

    week = 1;

    for ( ;; ) {
        next = njs_date_number_parse(&tm[NJS_DATE_DAY], p, end, 2);

        if (next != NULL) {
            /*
             * RFC 2822 format:
             *   "Mon, 28 Sep 1970 06:00:00 GMT",
             *   "Mon, 28 Sep 1970 06:00:00 UTC",
             *   "Mon, 28 Sep 1970 12:00:00 +0600".
             */
            return njs_date_rfc2822_string_parse(tm, next, end);
        }

        tm[NJS_DATE_MON] = njs_date_month_parse(p, end);

        if (tm[NJS_DATE_MON] >= 0) {
            /* Date.toString() format: "Mon Sep 28 1970 12:00:00 GMT+0600". */

            return njs_date_js_string_parse(tm, p + 3, end);
        }

        if (!week) {
            return NAN;
        }

        p = njs_date_skip_week_day(p, end);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        p = njs_date_skip_spaces(p, end);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        week = 0;
    }
}


static double
njs_date_rfc2822_string_parse(int64_t tm[], const u_char *p, const u_char *end)
{
    int64_t  gmtoff;

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    tm[NJS_DATE_MON] = njs_date_month_parse(p, end);
    if (njs_slow_path(tm[NJS_DATE_MON] < 0)) {
        return NAN;
    }

    p = njs_date_skip_spaces(p + 3, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_YR], p, end, 4);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    gmtoff = 0;

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_time_parse(tm, p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    if (njs_slow_path(p + 2 >= end)) {
        return NAN;
    }

    if ((p[0] == 'G' && p[1] == 'M' && p[2] == 'T')
        || (p[0] == 'U' && p[1] == 'T' && p[2] == 'C'))
    {
        gmtoff = 0;

    } else {
        gmtoff = njs_date_gmtoff_parse(p, end);

        if (njs_slow_path(gmtoff == -1)) {
            return NAN;
        }
    }

done:

    tm[NJS_DATE_MSEC] = -gmtoff * 60000;

    return njs_make_date(tm, 0);
}


static double
njs_date_js_string_parse(int64_t tm[], const u_char *p, const u_char *end)
{
    int64_t  gmtoff;

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_DAY], p, end, 2);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_YR], p, end, 4);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_time_parse(tm, p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    if (p + 2 < end && p[0] == 'G' && p[1] == 'M' && p[2] == 'T') {
        gmtoff = njs_date_gmtoff_parse(&p[3], end);

        if (njs_fast_path(gmtoff != -1)) {
            tm[NJS_DATE_MSEC] = -gmtoff * 60000;
            return njs_make_date(tm, 0);
        }
    }

    return NAN;

done:

    return njs_make_date(tm, 0);
}


static const u_char *
njs_date_skip_week_day(const u_char *p, const u_char *end)
{
    while (p < end) {
        if (*p == ' ') {
            return p;
        }

        p++;
    }

    return NULL;
}


static const u_char *
njs_date_skip_spaces(const u_char *p, const u_char *end)
{
    if (p < end && *p++ == ' ') {

        while (p < end) {
            if (*p != ' ') {
                return p;
            }

            p++;
        }

        return p;
    }

    return NULL;
}


static njs_int_t
njs_date_month_parse(const u_char *p, const u_char *end)
{
    if (p + 2 < end) {
        switch (p[0]) {

        case 'J':
            if (p[1] == 'a' && p[2] == 'n') {
                return 0;
            }

            if (p[1] == 'u') {
                if (p[2] == 'n') {
                    return 5;
                }

                if (p[2] == 'l') {
                    return 6;
                }
            }

            break;

        case 'F':
            if (p[1] == 'e' && p[2] == 'b') {
                return 1;
            }

            break;

        case 'M':
            if (p[1] == 'a') {
                if (p[2] == 'r') {
                    return 2;
                }

                if (p[2] == 'y') {
                    return 4;
                }
            }

            break;

        case 'A':
            if (p[1] == 'p' && p[2] == 'r') {
                return 3;
            }

            if (p[1] == 'u' && p[2] == 'g') {
                return 7;
            }

            break;

        case 'S':
            if (p[1] == 'e' && p[2] == 'p') {
                return 8;
            }

            break;

        case 'O':
            if (p[1] == 'c' && p[2] == 't') {
                return 9;
            }

            break;

        case 'N':
            if (p[1] == 'o' && p[2] == 'v') {
                return 10;
            }

            break;

        case 'D':
            if (p[1] == 'e' && p[2] == 'c') {
                return 11;
            }

            break;
        }
    }

    return -1;
}


static const u_char *
njs_date_time_parse(int64_t tm[], const u_char *p, const u_char *end)
{
    p = njs_date_number_parse(&tm[NJS_DATE_HR], p, end, 2);
    if (njs_slow_path(p == NULL)) {
        return p;
    }

    if (njs_slow_path(p >= end || *p != ':')) {
        return NULL;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_MIN], p + 1, end, 2);
    if (njs_slow_path(p == NULL)) {
        return p;
    }

    if (p == end) {
        return p;
    }

    if (njs_slow_path(*p != ':')) {
        return NULL;
    }

    return njs_date_number_parse(&tm[NJS_DATE_SEC], p + 1, end, 2);
}


static int64_t
njs_date_gmtoff_parse(const u_char *start, const u_char *end)
{
    int64_t       gmtoff, hour, min;
    const u_char  *p;

    if (njs_fast_path(start + 4 < end && (*start == '+' || *start == '-'))) {

        p = njs_date_number_parse(&hour, start + 1, end, 2);
        if (njs_slow_path(p == NULL)) {
            return -1;
        }

        p = njs_date_number_parse(&min, p, end, 2);
        if (njs_slow_path(p == NULL)) {
            return -1;
        }

        gmtoff = hour * 60 + min;

        if (*start == '-') {
            gmtoff = -gmtoff;
        }

        return gmtoff;
    }

    return -1;
}


static const u_char *
njs_date_number_parse(int64_t *value, const u_char *p, const u_char *end,
    size_t size)
{
    u_char     c;
    njs_int_t  n;

    n = 0;

    do {
        if (njs_slow_path(p >= end)) {
            return NULL;
        }

        c = *p++;

        /* Values below '0' become >= 208. */
        c = c - '0';

        if (njs_slow_path(c > 9)) {
            return NULL;
        }

        n = n * 10 + c;

        size--;

    } while (size != 0);

    *value = n;

    return p;
}


static const njs_object_prop_init_t  njs_date_constructor_properties[] =
{
    NJS_DECLARE_PROP_LENGTH(7),

    NJS_DECLARE_PROP_NAME("Date"),

    NJS_DECLARE_PROP_HANDLER(STRING_prototype, njs_object_prototype_create,
                             0, 0),

    NJS_DECLARE_PROP_NATIVE(STRING_UTC, njs_date_utc, 7, 0),

    NJS_DECLARE_PROP_NATIVE(STRING_now, njs_date_now, 0, 0),

    NJS_DECLARE_PROP_NATIVE(STRING_parse, njs_date_parse, 1, 0),
};


const njs_object_init_t  njs_date_constructor_init = {
    njs_date_constructor_properties,
    njs_nitems(njs_date_constructor_properties),
};


static njs_int_t
njs_date_prototype_value_of(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    njs_set_number(retval, njs_date(&args[0])->time);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_to_string(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t fmt, njs_value_t *retval)
{
    double  time;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    time = njs_date(&args[0])->time;

    if (fmt == NJS_DATE_FMT_TO_ISO_STRING && isnan(time)) {
        njs_range_error(vm, "Invalid time value");
        return NJS_ERROR;
    }

    return njs_date_string(vm, retval, fmt, time);
}


static njs_int_t
njs_date_string(njs_vm_t *vm, njs_value_t *retval, njs_date_fmt_t fmt,
    double time)
{
    int      year, tz;
    u_char   *p, sign;
    u_char   buf[NJS_DATE_TIME_LEN];
    int64_t  tm[NJS_DATE_MAX_FIELDS];

    static const char  *week[] = { "Sun", "Mon", "Tue", "Wed",
                                   "Thu", "Fri", "Sat" };

    static const char  *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

    if (njs_slow_path(isnan(time))) {
        njs_atom_to_value(vm, retval, NJS_ATOM_STRING_Invalid_Date);
        return NJS_OK;
    }

    p = buf;

    switch (fmt) {
    case NJS_DATE_FMT_TO_ISO_STRING:
    case NJS_DATE_FMT_TO_UTC_STRING:
        njs_destruct_date(time, tm, 0, 0);
        year = tm[NJS_DATE_YR];

        if (fmt == NJS_DATE_FMT_TO_UTC_STRING) {
            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                            "%s, %02L %s %04d %02L:%02L:%02L GMT",
                            week[tm[NJS_DATE_WDAY]], tm[NJS_DATE_DAY],
                            month[tm[NJS_DATE_MON]], year, tm[NJS_DATE_HR],
                            tm[NJS_DATE_MIN], tm[NJS_DATE_SEC]);

            break;
        }

        if (year >= 0 && year <= 9999) {
            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN, "%04d", year);

        } else {
            if (year > 0) {
                *p++ = '+';
            }

            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN, "%06d", year);
        }

        p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                        "-%02L-%02LT%02L:%02L:%02L.%03LZ",
                        tm[NJS_DATE_MON] + 1, tm[NJS_DATE_DAY], tm[NJS_DATE_HR],
                        tm[NJS_DATE_MIN], tm[NJS_DATE_SEC], tm[NJS_DATE_MSEC]);

        break;

    case NJS_DATE_FMT_TO_TIME_STRING:
    case NJS_DATE_FMT_TO_DATE_STRING:
    case NJS_DATE_FMT_TO_STRING:
    default:
        njs_destruct_date(time, tm, 0, 1);

        if (fmt != NJS_DATE_FMT_TO_TIME_STRING) {
            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                            "%s %s %02L %04L",
                            week[tm[NJS_DATE_WDAY]], month[tm[NJS_DATE_MON]],
                            tm[NJS_DATE_DAY], tm[NJS_DATE_YR]);
        }

        if (fmt != NJS_DATE_FMT_TO_DATE_STRING) {
            tz = -njs_tz_offset(time);
            sign = (tz < 0) ? '-' : '+';

            if (tz < 0) {
                tz = -tz;
            }

            if (p != buf) {
                *p++ = ' ';
            }

            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                            "%02L:%02L:%02L GMT%c%02d%02d",
                            tm[NJS_DATE_HR], tm[NJS_DATE_MIN], tm[NJS_DATE_SEC],
                            sign, tz / 60, tz % 60);
        }
    }

    return njs_string_new(vm, retval, buf, p - buf, p - buf);
}


njs_int_t
njs_date_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *date)
{
    if (njs_slow_path(!njs_is_date(date))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(date->type));

        return NJS_ERROR;
    }

    return njs_date_string(vm, retval, NJS_DATE_FMT_TO_ISO_STRING,
                           njs_date(date)->time);
}


static njs_int_t
njs_date_prototype_get_field(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t magic, njs_value_t *retval)
{
    double   value;
    int64_t  tm[NJS_DATE_MAX_FIELDS];

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    value = njs_date(&args[0])->time;

    if (njs_fast_path(!isnan(value))) {
        value = njs_destruct_date(value, tm, magic & 0xf, magic & 0x40);
    }

    njs_set_number(retval, value);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_get_timezone_offset(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
{
    double  value;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    value = njs_date(&args[0])->time;

    if (njs_fast_path(!isnan(value))) {
        value = njs_tz_offset(value);
    }

    njs_set_number(retval, value);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_set_time(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    double     time;
    njs_int_t  ret;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    if (nargs > 1) {
        if (njs_slow_path(!njs_is_number(&args[1]))) {
            ret = njs_value_to_numeric(vm, &args[1], &args[1]);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }
        }

        time = njs_timeclip(njs_number(&args[1]));

    } else {
        time = NAN;
    }

    njs_date(&args[0])->time = time;
    njs_set_number(retval, time);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_to_primitive(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
{
    njs_int_t   ret;
    njs_uint_t  hint;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    if (njs_slow_path(nargs <= 1)) {
        goto error;
    }

    if (njs_slow_path(!njs_is_string(&args[1]))) {
        ret = njs_value_to_string(vm, &args[1], &args[1]);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }

    if (args[1].atom_id == NJS_ATOM_STRING_unknown) {
        ret = njs_atom_atomize_key(vm, &args[1]);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }

    switch (args[1].atom_id) {
    case NJS_ATOM_STRING_number:
        hint = NJS_HINT_NUMBER;
        break;

    case NJS_ATOM_STRING_string:
    case NJS_ATOM_STRING_default:
        hint = NJS_HINT_STRING;
        break;

    default:
error:
        njs_type_error(vm, "invalid hint");
        return NJS_ERROR;

    }

    ret = njs_value_to_primitive(vm, &args[0], &args[0],
                                 hint | NJS_HINT_FORCE_ORDINARY);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    njs_value_assign(retval, &args[0]);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_set_fields(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t magic, njs_value_t *retval)
{
    double      time, num;
    njs_int_t   ret;
    njs_uint_t  since, left, i;
    int64_t     tm[NJS_DATE_MAX_FIELDS];

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    time = njs_date(&args[0])->time;

    since = magic & 7;

    if (njs_slow_path(nargs < 2 || (since != NJS_DATE_YR && isnan(time)))) {
        time = NAN;
        goto done;
    }

    i = 1;
    left = njs_min(((magic >> 3) & 7), nargs - 1);

    njs_destruct_date(time, tm, 0, magic & 0x40);

    do {
        ret = njs_value_to_number(vm, &args[i++], &num);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        if (!isfinite(num)) {
            tm[NJS_DATE_YR] = INT64_MIN;
            continue;
        }

        tm[since++] = njs_number_to_integer(num);

    } while (--left);

    time = njs_make_date(tm, 1);

done:

    njs_date(&args[0])->time = time;
    njs_set_number(retval, time);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_to_json(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t            ret;
    njs_value_t          value;
    njs_flathsh_query_t  fhq;

    if (njs_is_object(njs_argument(args, 0))) {
        fhq.proto = &njs_object_hash_proto;
        fhq.key_hash = NJS_ATOM_STRING_toISOString;

        ret = njs_object_property(vm, njs_object(njs_argument(args, 0)), &fhq,
                                  &value);

        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }

        if (njs_is_function(&value)) {
            return njs_function_apply(vm, njs_function(&value), args, nargs,
                                      retval);
        }
    }

    njs_type_error(vm, "\"this\" argument is not an object");

    return NJS_ERROR;
}


static const njs_object_prop_init_t  njs_date_prototype_properties[] =
{
    NJS_DECLARE_PROP_HANDLER(STRING___proto__,
                             njs_primitive_prototype_get_proto, 0,
                             NJS_OBJECT_PROP_VALUE_CW),

    NJS_DECLARE_PROP_HANDLER(STRING_constructor,
                             njs_object_prototype_create_constructor, 0,
                             NJS_OBJECT_PROP_VALUE_CW),

    NJS_DECLARE_PROP_NATIVE(STRING_valueOf, njs_date_prototype_value_of, 0,
                            0),

    NJS_DECLARE_PROP_NATIVE(STRING_toString, njs_date_prototype_to_string,
                            0, NJS_DATE_FMT_TO_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toDateString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_DATE_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toTimeString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_TIME_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toLocaleString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toLocaleDateString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_DATE_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toLocaleTimeString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_TIME_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toUTCString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_UTC_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toISOString,
                            njs_date_prototype_to_string, 0,
                            NJS_DATE_FMT_TO_ISO_STRING),

    NJS_DECLARE_PROP_NATIVE(STRING_toJSON, njs_date_prototype_to_json, 1,
                            0),

    NJS_DECLARE_PROP_NATIVE(STRING_getTime, njs_date_prototype_value_of, 0,
                            0),

    NJS_DECLARE_PROP_NATIVE(STRING_getFullYear,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_YR, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCFullYear,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_YR, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getMonth,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_MON, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCMonth,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_MON, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getDate,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_DAY, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCDate,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_DAY, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getDay,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_WDAY, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCDay,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_WDAY, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getHours,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_HR, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCHours,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_HR, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getMinutes,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_MIN, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCMinutes,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_MIN, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getSeconds,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_SEC, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCSeconds,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_SEC, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getMilliseconds,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_MSEC, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_getUTCMilliseconds,
                            njs_date_prototype_get_field, 0,
                            njs_date_magic(NJS_DATE_MSEC, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_getTimezoneOffset,
                            njs_date_prototype_get_timezone_offset, 0, 0),

    NJS_DECLARE_PROP_NATIVE(STRING_setTime, njs_date_prototype_set_time, 1,
                            0),

    NJS_DECLARE_PROP_NATIVE(STRING_setMilliseconds,
                            njs_date_prototype_set_fields, 1,
                            njs_date_magic2(NJS_DATE_MSEC, 1, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCMilliseconds,
                            njs_date_prototype_set_fields, 1,
                            njs_date_magic2(NJS_DATE_MSEC, 1, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_setSeconds,
                            njs_date_prototype_set_fields, 2,
                            njs_date_magic2(NJS_DATE_SEC, 2, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCSeconds,
                            njs_date_prototype_set_fields, 2,
                            njs_date_magic2(NJS_DATE_SEC, 2, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_setMinutes,
                            njs_date_prototype_set_fields, 3,
                            njs_date_magic2(NJS_DATE_MIN, 3, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCMinutes,
                            njs_date_prototype_set_fields, 3,
                            njs_date_magic2(NJS_DATE_MIN, 3, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_setHours,
                            njs_date_prototype_set_fields, 4,
                            njs_date_magic2(NJS_DATE_HR, 4, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCHours,
                            njs_date_prototype_set_fields, 4,
                            njs_date_magic2(NJS_DATE_HR, 4, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_setDate,
                            njs_date_prototype_set_fields, 1,
                            njs_date_magic2(NJS_DATE_DAY, 1, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCDate,
                            njs_date_prototype_set_fields, 1,
                            njs_date_magic2(NJS_DATE_DAY, 1, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_setMonth,
                            njs_date_prototype_set_fields, 2,
                            njs_date_magic2(NJS_DATE_MON, 2, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCMonth,
                            njs_date_prototype_set_fields, 2,
                            njs_date_magic2(NJS_DATE_MON, 2, 0)),

    NJS_DECLARE_PROP_NATIVE(STRING_setFullYear,
                            njs_date_prototype_set_fields, 3,
                            njs_date_magic2(NJS_DATE_YR, 3, 1)),

    NJS_DECLARE_PROP_NATIVE(STRING_setUTCFullYear,
                            njs_date_prototype_set_fields, 3,
                            njs_date_magic2(NJS_DATE_YR, 3, 0)),

    /* NJS_DECLARE_PROP_NATIVE, but not writable */
    NJS_DECLARE_PROP_VALUE(SYMBOL_toPrimitive,
                           njs_native_function2(njs_date_prototype_to_primitive,
                                                1, 0),
                           NJS_OBJECT_PROP_VALUE_C),
};


const njs_object_init_t  njs_date_prototype_init = {
    njs_date_prototype_properties,
    njs_nitems(njs_date_prototype_properties),
};


const njs_object_type_init_t  njs_date_type_init = {
   .constructor = njs_native_ctor(njs_date_constructor, 7, 0),
   .constructor_props = &njs_date_constructor_init,
   .prototype_props = &njs_date_prototype_init,
   .prototype_value = { .object = { .type = NJS_OBJECT } },
};

