/**
 * Copyright (c) André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */
package com.github.anba.es6draft.runtime.objects.date;

import static com.github.anba.es6draft.runtime.AbstractOperations.ToInteger;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.github.anba.es6draft.runtime.Realm;

/**
 * <h1>20 Numbers and Dates</h1><br>
 * <h2>20.3 Date Objects</h2>
 * <ul>
 * <li>20.3.1 Overview of Date Objects and Definitions of Abstract Operations
 * </ul>
 */
public final class DateAbstractOperations {
    private DateAbstractOperations() {
    }

    /**
     * 5.2 Algorithm Conventions
     * 
     * @param dividend
     *            the dividend
     * @param divisor
     *            the divisor
     * @return the modulo result
     */
    private static double modulo(double dividend, double divisor) {
        assert divisor != 0 && isFinite(divisor);
        double remainder = dividend % divisor;
        // NB: add +0 to convert -0 to +0
        return (remainder >= 0 ? remainder + (+0d) : remainder + divisor);
    }

    /**
     * 20.3.1.2 Day Number and Time within Day
     */
    public static final double msPerDay = 86400000;

    /**
     * 20.3.1.2 Day Number and Time within Day
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the number of days since the epoch
     */
    public static double Day(double t) {
        return Math.floor(t / msPerDay);
    }

    /**
     * 20.3.1.2 Day Number and Time within Day
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the number of milli-seconds since midnight
     */
    public static double TimeWithinDay(double t) {
        return modulo(t, msPerDay);
    }

    /**
     * 20.3.1.3 Year Number
     * 
     * @param y
     *            the year
     * @return the number of days
     */
    public static double DaysInYear(double y) {
        if (y % 4 != 0) {
            return 365;
        }
        if (y % 100 != 0) {
            return 366;
        }
        if (y % 400 != 0) {
            return 365;
        }
        return 366;
    }

    /**
     * 20.3.1.3 Year Number
     * 
     * @param y
     *            the year
     * @return the number of days since the epoch
     */
    public static double DayFromYear(double y) {
        return 365 * (y - 1970) + Math.floor((y - 1969) / 4) - Math.floor((y - 1901) / 100)
                + Math.floor((y - 1601) / 400);
    }

    /**
     * 20.3.1.3 Year Number
     * 
     * @param y
     *            the year
     * @return the number of milli-seconds since the epoch
     */
    public static double TimeFromYear(double y) {
        return msPerDay * DayFromYear(y);
    }

    /**
     * 20.3.1.3 Year Number
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the year
     */
    public static double YearFromTime(double t) {
        double y = 1970 + Math.floor(t / (365 * msPerDay));
        double e = TimeFromYear(y);
        if (e > t) {
            do {
                y -= 1;
            } while (TimeFromYear(y) > t);
        } else if (e + DaysInYear(y) * msPerDay <= t) {
            do {
                y += 1;
            } while (TimeFromYear(y) + DaysInYear(y) * msPerDay <= t);
        }
        return y;
    }

    /**
     * 20.3.1.3 Year Number
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return {@code true} if the year is a leap year
     */
    public static boolean InLeapYear(double t) {
        return DaysInYear(YearFromTime(t)) == 366;
    }

    /**
     * 20.3.1.4 Month Number
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the month
     */
    public static double MonthFromTime(double t) {
        double d = DayWithinYear(t);
        double leap = InLeapYear(t) ? 1 : 0;

        if (0 <= d && d < 31) {
            return 0;
        }
        if (31 <= d && d < 59 + leap) {
            return 1;
        }
        if (59 + leap <= d && d < 90 + leap) {
            return 2;
        }
        if (90 + leap <= d && d < 120 + leap) {
            return 3;
        }
        if (120 + leap <= d && d < 151 + leap) {
            return 4;
        }
        if (151 + leap <= d && d < 181 + leap) {
            return 5;
        }
        if (181 + leap <= d && d < 212 + leap) {
            return 6;
        }
        if (212 + leap <= d && d < 243 + leap) {
            return 7;
        }
        if (243 + leap <= d && d < 273 + leap) {
            return 8;
        }
        if (273 + leap <= d && d < 304 + leap) {
            return 9;
        }
        if (304 + leap <= d && d < 334 + leap) {
            return 10;
        }
        if (334 + leap <= d && d < 365 + leap) {
            return 11;
        }
        return Double.NaN;
    }

    /**
     * 20.3.1.4 Month Number
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the number of days since new year
     */
    public static double DayWithinYear(double t) {
        return Day(t) - DayFromYear(YearFromTime(t));
    }

    /**
     * 20.3.1.5 Date Number
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the number of days since the epoch
     */
    public static double DateFromTime(double t) {
        double m = MonthFromTime(t);
        if (Double.isNaN(m)) {
            return Double.NaN;
        }
        assert (int) m == m && m >= 0 && m <= 11;
        double leap = InLeapYear(t) ? 1 : 0;
        switch ((int) m) {
        case 0:
            return DayWithinYear(t) + 1;
        case 1:
            return DayWithinYear(t) - 30;
        case 2:
            return DayWithinYear(t) - 58 - leap;
        case 3:
            return DayWithinYear(t) - 89 - leap;
        case 4:
            return DayWithinYear(t) - 119 - leap;
        case 5:
            return DayWithinYear(t) - 150 - leap;
        case 6:
            return DayWithinYear(t) - 180 - leap;
        case 7:
            return DayWithinYear(t) - 211 - leap;
        case 8:
            return DayWithinYear(t) - 242 - leap;
        case 9:
            return DayWithinYear(t) - 272 - leap;
        case 10:
            return DayWithinYear(t) - 303 - leap;
        case 11:
            return DayWithinYear(t) - 333 - leap;
        default:
            throw new AssertionError();
        }
    }

    /**
     * 20.3.1.6 Week Day
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the week day
     */
    public static double WeekDay(double t) {
        return modulo(Day(t) + 4, 7);
    }

    /**
     * 20.3.1.7 Local Time Zone Adjustment
     * 
     * @param realm
     *            the realm instance
     * @return the locale time zone offset
     */
    public static double LocalTZA(Realm realm) {
        return TimeZoneInfo.getDefault().getRawOffset(realm.getTimeZone());
    }

    /**
     * 20.3.1.8 Daylight Saving Time Adjustment
     * 
     * @param realm
     *            the realm instance
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the day light saving time in milli-seconds
     */
    public static double DaylightSavingTA(Realm realm, double t) {
        if (Double.isNaN(t)) {
            return t;
        }
        assert Math.abs(t) <= 8.64e15;
        return TimeZoneInfo.getDefault().getDSTSavings(realm.getTimeZone(), (long) t);
    }

    /**
     * 20.3.1.9 LocalTime ( t )
     * 
     * @param realm
     *            the realm instance
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the local time value in milli-seconds
     */
    public static double LocalTime(Realm realm, double t) {
        // return t + LocalTZA(realm) + DaylightSavingTA(realm, t);
        if (Double.isNaN(t)) {
            return t;
        }
        assert Math.abs(t) <= 8.64e15;
        return TimeZoneInfo.getDefault().localTime(realm.getTimeZone(), (long) t);
    }

    /**
     * 20.3.1.10 UTC ( t )
     * 
     * @param realm
     *            the realm instance
     * @param t
     *            the local time value in milli-seconds
     * @return the date in milli-seconds since the epoch
     */
    public static double UTC(Realm realm, double t) {
        // double d = t - LocalTZA(realm);
        // return d - DaylightSavingTA(realm, d - realm.getTimezone().getDSTSavings());
        // TODO: spec issue - https://bugs.ecmascript.org/show_bug.cgi?id=4007
        // return t - LocalTZA(realm) - DaylightSavingTA(realm, t - LocalTZA(realm));
        if (Double.isNaN(t) || Math.abs(t) > (8.64e15 + 8.64e7)) {
            return t;
        }
        return TimeZoneInfo.getDefault().utc(realm.getTimeZone(), (long) t);
    }

    /**
     * 20.3.1.11 Hours, Minutes, Second, and Milliseconds
     */
    // @formatter:off
    public static final double
        HoursPerDay = 24,
        MinutesPerHour = 60,
        SecondsPerMinute = 60,
        msPerSecond = 1000,
        msPerMinute = msPerSecond * SecondsPerMinute,
        msPerHour = msPerMinute * MinutesPerHour
        ;
    // @formatter:on

    /**
     * 20.3.1.11 Hours, Minutes, Second, and Milliseconds
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the hours
     */
    public static double HourFromTime(double t) {
        return modulo(Math.floor(t / msPerHour), HoursPerDay);
    }

    /**
     * 20.3.1.11 Hours, Minutes, Second, and Milliseconds
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the minutes
     */
    public static double MinFromTime(double t) {
        return modulo(Math.floor(t / msPerMinute), MinutesPerHour);
    }

    /**
     * 20.3.1.11 Hours, Minutes, Second, and Milliseconds
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the seconds
     */
    public static double SecFromTime(double t) {
        return modulo(Math.floor(t / msPerSecond), SecondsPerMinute);
    }

    /**
     * 20.3.1.11 Hours, Minutes, Second, and Milliseconds
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the milli-seconds
     */
    public static double msFromTime(double t) {
        return modulo(t, msPerSecond);
    }

    private static boolean isFinite(double d) {
        return !(Double.isNaN(d) || Double.isInfinite(d));
    }

    /**
     * 20.3.1.12 MakeTime (hour, min, sec, ms)
     * 
     * @param hour
     *            the hour
     * @param min
     *            the minutes
     * @param sec
     *            the seconds
     * @param ms
     *            the milli-seconds
     * @return the date in milli-seconds
     */
    public static double MakeTime(double hour, double min, double sec, double ms) {
        if (!isFinite(hour) || !isFinite(min) || !isFinite(sec) || !isFinite(ms)) {
            return Double.NaN;
        }
        double h = ToInteger(hour);
        double m = ToInteger(min);
        double s = ToInteger(sec);
        double milli = ToInteger(ms);
        return h * msPerHour + m * msPerMinute + s * msPerSecond + milli;
    }

    /**
     * 20.3.1.13 MakeDay (year, month, date)
     * 
     * @param year
     *            the year
     * @param month
     *            the month
     * @param date
     *            the date
     * @return the number of days
     */
    public static double MakeDay(double year, double month, double date) {
        if (!isFinite(year) || !isFinite(month) || !isFinite(date)) {
            return Double.NaN;
        }
        double y = ToInteger(year);
        double m = ToInteger(month);
        double dt = ToInteger(date);
        // FIXME: spec issue - `y + Math.floor(m / 12)` could produce Inf if y and m are near Number.MAX_VALUE.
        double ym = y + Math.floor(m / 12);
        double mn = modulo(m, 12);

        // FIXME: spec issue - unclear spec comment (step 7):
        // "[...]; but if this is not possible (because some argument is out of range), return NaN."
        double[] monthStart = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
        double day = Math.floor(TimeFromYear(ym) / msPerDay) + monthStart[(int) mn];
        if (mn >= 2 && DaysInYear(ym) == 366) {
            day += 1;
        }

        return day + dt - 1;
    }

    /**
     * 20.3.1.14 MakeDate (day, time)
     * 
     * @param day
     *            the days
     * @param time
     *            the time in milli-seconds
     * @return the date in milli-seconds
     */
    public static double MakeDate(double day, double time) {
        if (!isFinite(day) || !isFinite(time)) {
            return Double.NaN;
        }
        return day * msPerDay + time;
    }

    /**
     * 20.3.1.15 TimeClip (time)
     * 
     * @param time
     *            the time in milli-seconds
     * @return the clipped time in milli-seconds
     */
    public static double TimeClip(double time) {
        if (!isFinite(time)) {
            return Double.NaN;
        }
        if (Math.abs(time) > 8.64e15) {
            return Double.NaN;
        }
        return ToInteger(time) + (+0d);
    }

    private static int DaysInMonth(int year, int month) {
        // month is 1-based for DaysInMonth!
        if (month == 2)
            return DaysInYear(year) == 366 ? 29 : 28;
        return month >= 8 ? 31 - (month & 1) : 30 + (month & 1);
    }

    /**
     * 20.3.1.16 Date Time String Format<br>
     * Parses the input string according to the simplified ISO-8601 Extended Format:
     * <ul>
     * <li><code>YYYY-MM-DD'T'HH:mm:ss.sss'Z'</code></li>
     * <li>or <code>YYYY-MM-DD'T'HH:mm:ss.sss[+-]hh:mm</code></li>
     * </ul>
     * 
     * @param realm
     *            the realm instance
     * @param s
     *            the string
     * @param lenient
     *            the lenient flag
     * @return the date in milli-seconds or {@code NaN} if not parsed successfully
     */
    public static double parseISOString(Realm realm, String s, boolean lenient) {
        // use a simple state machine to parse the input string
        final int ERROR = -1;
        final int YEAR = 0, MONTH = 1, DAY = 2;
        final int HOUR = 3, MIN = 4, SEC = 5, MSEC = 6;
        final int TZHOUR = 7, TZMIN = 8;
        int state = YEAR;
        // default values per [20.3.1.15 Date Time String Format]
        int[] values = { 1970, 1, 1, 0, 0, 0, 0, -1, -1 };
        int yearlen = 4, yearmod = 1, tzmod = 1;
        int i = 0, len = s.length();
        if (len != 0) {
            char c = s.charAt(0);
            if (c == '+' || c == '-') {
                // 20.3.1.15.1 Extended years
                i += 1;
                yearlen = 6;
                yearmod = (c == '-') ? -1 : 1;
            } else if (c == 'T') {
                // time-only forms no longer in spec, but follow spidermonkey here
                i += 1;
                state = HOUR;
            }
        }
        loop: while (state != ERROR) {
            if (state != MSEC) {
                int m = i + (state == YEAR ? yearlen : 2);
                if (m > len) {
                    state = ERROR;
                    break;
                }

                int value = 0;
                for (; i < m; ++i) {
                    char c = s.charAt(i);
                    if (c < '0' || c > '9') {
                        state = ERROR;
                        break loop;
                    }
                    value = 10 * value + (c - '0');
                }
                values[state] = value;
            } else {
                // common extension: 1..n milliseconds
                if (i >= len) {
                    state = ERROR;
                    break;
                }

                // no common behaviour: truncate or round?
                double value = 0, f = 0.1;
                for (int start = i; i < len; ++i, f *= 0.1) {
                    char c = s.charAt(i);
                    if (c < '0' || c > '9') {
                        if (i == start) {
                            state = ERROR;
                            break loop;
                        }
                        break;
                    }
                    value += f * (c - '0');
                }
                values[state] = (int) (1000 * value);
            }

            if (i == len) {
                // reached EOF, check for end state
                switch (state) {
                case HOUR:
                case TZHOUR:
                    state = ERROR;
                }
                break;
            }

            char c = s.charAt(i++);
            if (c == 'Z') {
                // handle abbrevation for UTC timezone
                values[TZHOUR] = 0;
                values[TZMIN] = 0;
                switch (state) {
                case MIN:
                case SEC:
                case MSEC:
                    break;
                default:
                    state = ERROR;
                }
                break;
            }

            // state transition
            switch (state) {
            case YEAR:
            case MONTH:
                state = (c == '-' ? state + 1 : c == 'T' ? HOUR : ERROR);
                break;
            case DAY:
                // allow ' ' as time separator in lenient mode
                state = (c == 'T' ? HOUR : lenient && c == ' ' ? HOUR : ERROR);
                break;
            case HOUR:
                state = (c == ':' ? MIN : ERROR);
                break;
            case TZHOUR:
                // state = (c == ':' ? state + 1 : ERROR);
                // Non-standard extension, https://bugzilla.mozilla.org/show_bug.cgi?id=682754
                if (c != ':') {
                    // back off by one and try to read without ':' separator
                    i -= 1;
                }
                state = TZMIN;
                break;
            case MIN:
                state = (c == ':' ? SEC : c == '+' || c == '-' ? TZHOUR : ERROR);
                break;
            case SEC:
                state = (c == '.' ? MSEC : c == '+' || c == '-' ? TZHOUR : ERROR);
                break;
            case MSEC:
                state = (c == '+' || c == '-' ? TZHOUR : ERROR);
                break;
            case TZMIN:
                state = ERROR;
                break;
            }
            if (state == TZHOUR) {
                // save timezone modificator
                tzmod = (c == '-') ? -1 : 1;
            }
        }

        syntax: {
            // error or unparsed characters
            if (state == ERROR || i != len)
                break syntax;

            // check values
            int year = values[YEAR], month = values[MONTH], day = values[DAY];
            int hour = values[HOUR], min = values[MIN], sec = values[SEC], msec = values[MSEC];
            int tzhour = values[TZHOUR], tzmin = values[TZMIN];
            if (year > 275943 // ceil(1e8/365) + 1970 = 275943
                    || (month < 1 || month > 12) || (day < 1 || day > DaysInMonth(year, month)) || hour > 24
                    || (hour == 24 && (min > 0 || sec > 0 || msec > 0)) || min > 59 || sec > 59 || tzhour > 23
                    || tzmin > 59) {
                break syntax;
            }
            // valid ISO-8601 format, compute date in milliseconds
            double date = MakeDate(MakeDay(year * yearmod, month - 1, day), MakeTime(hour, min, sec, msec));
            if (tzhour == -1) {
                if (!(state == YEAR || state == MONTH || state == DAY)) {
                    // if time zone offset absent, interpret date-time as a local time
                    date = UTC(realm, date);
                }
            } else {
                date -= (tzhour * 60 + tzmin) * msPerMinute * tzmod;
            }

            if (date < -8.64e15 || date > 8.64e15)
                break syntax;
            return date;
        }

        // invalid ISO-8601 format, return NaN
        return Double.NaN;
    }

    private static final String[] weekDayNames = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

    /**
     * Week Day Name
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the week day name
     */
    public static String WeekDayName(double t) {
        assert !Double.isNaN(t);
        double weekDay = WeekDay(t);
        return weekDayNames[(int) weekDay];
    }

    private static final String[] monthNames = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
            "Nov", "Dec" };

    /**
     * Month Name
     * 
     * @param t
     *            the date in milli-seconds since the epoch
     * @return the month name
     */
    public static String MonthNameFromTime(double t) {
        assert !Double.isNaN(t);
        double month = MonthFromTime(t);
        return monthNames[(int) month];
    }

    private static final Pattern utcDateTimePattern;
    private static final Pattern dateTimePattern;
    private static final Pattern usDateTimePattern;

    static {
        String weekday = "(?:([a-zA-Z]{3}),? )?";
        String time = "(?: ([0-2]?[0-9]):([0-5][0-9])(?::([0-5][0-9]))?(?: (AM|PM))?)?";
        String timezone;
        {
            String tzHour = "([+-][0-9]{2})";
            String tzMin = "([0-9]{2})";
            String tzOffset = "(?: ?" + tzHour + ":?" + tzMin + ")?";
            String tzName = "(?: \\([a-zA-Z]{3,5}\\))?";
            timezone = "( (?:GMT|UTC)?" + tzOffset + tzName + ")?";
        }

        // "EEE, dd MMM yyyy HH:mm:ss 'AM|PM' 'GMT'Z (z)"
        String utcDate = "([0-3]?[0-9]) ([a-zA-Z]{3}) (-?[0-9]{1,6})";
        utcDateTimePattern = Pattern.compile(weekday + utcDate + time + timezone);

        // "EEE, MMM dd yyyy HH:mm:ss 'AM|PM' 'GMT'Z (z)"
        String date = "([a-zA-Z]{3}) ([0-3]?[0-9]) (-?[0-9]{1,6})";
        dateTimePattern = Pattern.compile(weekday + date + time + timezone);

        // "mm/dd/yyyy HH:mm:ss 'AM|PM'"
        String usDate = "([0-1]?[0-9])/([0-3]?[0-9])/([0-9]{1,6})";
        usDateTimePattern = Pattern.compile(usDate + time);
    }

    /**
     * Parses a date-time string in "EEE MMM dd yyyy HH:mm:ss 'GMT'Z (z)" format, returns {@link Double#NaN} on
     * mismatch.
     * 
     * @param realm
     *            the realm instance
     * @param s
     *            the string
     * @return the date in milli-seconds or {@code NaN} if not parsed successfully
     */
    public static double parseDateString(Realm realm, String s) {
        Matcher matcher;
        if ((matcher = utcDateTimePattern.matcher(s)).matches()) {
            assert matcher.groupCount() == 11;
            double day = fromDateString(matcher.group(4), matcher.group(3), matcher.group(2), matcher.group(1));
            double time = fromTimeString(matcher.group(5), matcher.group(6), matcher.group(7), matcher.group(8));
            double tzOffset = 0;
            boolean localTime = matcher.group(9) == null;
            if (!localTime) {
                tzOffset = fromTimeZoneString(matcher.group(10), matcher.group(11));
            }
            return fromDateTimeString(realm, day, time, tzOffset, localTime);
        }
        if ((matcher = dateTimePattern.matcher(s)).matches()) {
            assert matcher.groupCount() == 11;
            double day = fromDateString(matcher.group(4), matcher.group(2), matcher.group(3), matcher.group(1));
            double time = fromTimeString(matcher.group(5), matcher.group(6), matcher.group(7), matcher.group(8));
            double tzOffset = 0;
            boolean localTime = matcher.group(9) == null;
            if (!localTime) {
                tzOffset = fromTimeZoneString(matcher.group(10), matcher.group(11));
            }
            return fromDateTimeString(realm, day, time, tzOffset, localTime);
        }
        if ((matcher = usDateTimePattern.matcher(s)).matches()) {
            assert matcher.groupCount() == 7;
            double day = fromDateString(matcher.group(3), matcher.group(1), matcher.group(2));
            double time = fromTimeString(matcher.group(4), matcher.group(5), matcher.group(6), matcher.group(7));
            double tzOffset = 0;
            boolean localTime = true;
            return fromDateTimeString(realm, day, time, tzOffset, localTime);
        }
        return Double.NaN;
    }

    private static double fromDateTimeString(Realm realm, double day, double time, double tzOffset, boolean localTime) {
        if (Double.isNaN(day) || Double.isNaN(time) || Double.isNaN(tzOffset))
            return Double.NaN;
        double date = MakeDate(day, time);
        if (localTime) {
            date = UTC(realm, date);
        } else {
            date -= tzOffset;
        }
        if (date < -8.64e15 || date > 8.64e15)
            return Double.NaN;
        return date;
    }

    private static double fromDateString(String yearValue, String monthName, String dayValue, String weekdayName) {
        assert yearValue != null && monthName != null && dayValue != null;
        int month = 1 + indexOf(monthNames, monthName);
        if (weekdayName != null) {
            // Just parse, but ignore actual value.
            int weekday = indexOf(weekDayNames, weekdayName);
            if (weekday == -1) {
                return Double.NaN;
            }
        }
        int year = Integer.parseInt(yearValue);
        int day = Integer.parseInt(dayValue);
        return fromDateString(year, month, day);
    }

    private static double fromDateString(String yearValue, String monthValue, String dayValue) {
        assert yearValue != null && monthValue != null && dayValue != null;
        int year = Integer.parseInt(yearValue);
        int month = Integer.parseInt(monthValue);
        int day = Integer.parseInt(dayValue);
        return fromDateString(year, month, day);
    }

    private static double fromDateString(int year, int month, int day) {
        if (Math.abs(year) > 275943 // ceil(1e8/365) + 1970 = 275943
                || (month < 1 || month > 12) || (day < 1 || day > DaysInMonth(year, month))) {
            return Double.NaN;
        }
        return MakeDay(year, month - 1, day);
    }

    private static double fromTimeString(String hourValue, String minValue, String secValue, String amPm) {
        if (hourValue == null) {
            assert minValue == null && secValue == null && amPm == null;
            return MakeTime(0, 0, 0, 0);
        }
        assert minValue != null;
        int hour = Integer.parseInt(hourValue);
        int min = Integer.parseInt(minValue);
        int sec = secValue != null ? Integer.parseInt(secValue) : 0;
        if (amPm == null) {
            if (hour > 24 || (hour == 24 && (min > 0 || sec > 0)) || min > 59 || sec > 59) {
                return Double.NaN;
            }
        } else {
            if (hour > 12 || min > 59 || sec > 59) {
                return Double.NaN;
            }
            if (hour == 12) {
                if ("AM".equals(amPm)) {
                    hour = 0;
                }
            } else {
                if ("PM".equals(amPm)) {
                    hour += 12;
                }
            }
        }
        return MakeTime(hour, min, sec, 0);
    }

    private static double fromTimeZoneString(String tzHourValue, String tzMinValue) {
        if (tzHourValue == null) {
            assert tzMinValue == null;
            return 0;
        }
        assert tzMinValue != null;
        int tzhour = Integer.parseInt(tzHourValue);
        int tzmin = Integer.parseInt(tzMinValue);
        if (Math.abs(tzhour) > 23 || Math.abs(tzmin) > 59) {
            return Double.NaN;
        }
        if (tzhour < 0) {
            return (tzhour * 60 - tzmin) * msPerMinute;
        }
        return (tzhour * 60 + tzmin) * msPerMinute;
    }

    private static int indexOf(String[] array, String value) {
        for (int i = 0, len = array.length; i < len; ++i) {
            if (array[i].equals(value)) {
                return i;
            }
        }
        return -1;
    }
}
