/** * Copyright (c) 2012-2016 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> */ 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 */ public static final double // HoursPerDay = 24, // MinutesPerHour = 60, // SecondsPerMinute = 60, // msPerSecond = 1000, // msPerMinute = msPerSecond * SecondsPerMinute, // msPerHour = msPerMinute * MinutesPerHour; /** * 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); double ym = y + Math.floor(m / 12); double mn = modulo(m, 12); 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; } }