lib/VM/JSLib/DateUtil.cpp (688 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/VM/JSLib/DateUtil.h" #include "hermes/Platform/Unicode/PlatformUnicode.h" #include "hermes/Support/Compiler.h" #include "hermes/Support/OSCompat.h" #include "hermes/VM/CallResult.h" #include "hermes/VM/JSLib/RuntimeCommonStorage.h" #include "hermes/VM/SmallXString.h" #include "llvh/Support/ErrorHandling.h" #include "llvh/Support/Format.h" #include "llvh/Support/raw_ostream.h" #include <cassert> #include <cctype> #include <cmath> #include <ctime> namespace hermes { namespace vm { /// Set \p quot to the largest integral value that is smaller than or equal to /// the algebraic quotient of \p x divided by \p y. /// Set \p rem to the floor modulus of \p x divided by \p y. static void floorDivMod(int64_t x, int64_t y, int64_t *quot, int64_t *rem) { int64_t q = x / y; // signs are different && not evenly divisable if ((x ^ y) < 0 && q * y != x) { q--; } *quot = q; *rem = x - y * q; } /// \return the floor modulus of \p x divided by \p y. static int64_t floorMod(int64_t x, int64_t y) { int64_t quot, rem; floorDivMod(x, y, &quot, &rem); return rem; } /// Perform the fmod operation and adjusts the result so that it's not negative. /// Useful in computing dates before Jan 1 1970. static inline double posfmod(double x, double y) { double result = std::fmod(x, y); return result < 0 ? result + y : result; } //===----------------------------------------------------------------------===// // Current time std::chrono::milliseconds::rep curTime() { // Use std::chrono here because we need millisecond precision, which // std::time() fails to provide. return std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::system_clock::now().time_since_epoch()) .count(); } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.2 double day(double t) { return std::floor(t / MS_PER_DAY); } double timeWithinDay(double t) { return posfmod(t, MS_PER_DAY); } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.3 /// \return true if year \p y is a leap year. static bool isLeapYear(double y) { if (std::fmod(y, 4) != 0) { return false; } // y % 4 == 0 if (std::fmod(y, 100) != 0) { return true; } // y % 100 == 0 if (std::fmod(y, 400) != 0) { return false; } // y % 400 == 0 return true; } /// \return true if year \p y is a leap year. static bool isLeapYear(int32_t y) { if (y % 4 != 0) { return false; } // y % 4 == 0 if (y % 100 != 0) { return true; } // y % 100 == 0 if (y % 400 != 0) { return false; } // y % 400 == 0 return true; } uint32_t daysInYear(double y) { return isLeapYear(y) ? 366 : 365; } double dayFromYear(double y) { // Use the formula given in the spec for computing the day from year. return 365 * (y - 1970) + std::floor((y - 1969) / 4.0) - std::floor((y - 1901) / 100.0) + std::floor((y - 1601) / 400.0); } double timeFromYear(double y) { return MS_PER_DAY * dayFromYear(y); } double yearFromTime(double t) { if (!std::isfinite(t)) { // If t is infinitely in the future be done. return t; } // Estimate y using the average year length. double y = std::floor(t / (MS_PER_DAY * 365.2425)) + 1970; // Actual time for year y. double yt = timeFromYear(y); while (yt > t) { // Estimate was too high, decrement until we're correct. --y; yt = timeFromYear(y); } while (yt + daysInYear(y) * MS_PER_DAY <= t) { // t is more than a year away from the start of y. // Increment y until we're correct. ++y; yt = timeFromYear(y); } assert( timeFromYear(y) <= t && timeFromYear(y + 1) > t && "yearFromTime incorrect"); return y; } bool inLeapYear(double t) { return daysInYear(yearFromTime(t)) == 366; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.4 uint32_t monthFromTime(double t) { double dayWithinYear = day(t) - dayFromYear(yearFromTime(t)); constexpr int8_t kDaysInMonthNonLeap[11] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30}; double curDay = 0.0; for (uint32_t i = 0; i < 11; ++i) { curDay += (i == 1 && inLeapYear(t)) ? kDaysInMonthNonLeap[i] + 1 : kDaysInMonthNonLeap[i]; if (dayWithinYear < curDay) return i; } // Must be December. return 11; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.5 /// Gives the offset of the first day in month m. /// \param leap indicates if \p m falls in a leap year. static uint32_t dayFromMonth(uint32_t m, bool leap) { static const uint16_t standardTable[]{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; static const uint16_t leapYearTable[]{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}; assert(m < 12 && "invalid month supplied to dayFromMonth"); return leap ? leapYearTable[m] : standardTable[m]; } double dateFromTime(double t) { double dayWithinYear = day(t) - dayFromYear(yearFromTime(t)); bool leap = inLeapYear(t); return dayWithinYear - dayFromMonth(monthFromTime(t), leap) + 1; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.6 int32_t weekDay(double t) { return posfmod((day(t) + 4), 7); } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.7 double localTZA() { #ifdef _WINDOWS _tzset(); long gmtoff; int err = _get_timezone(&gmtoff); assert(err == 0 && "_get_timezone failed in localTZA()"); // The result of _get_timezone is negated return -gmtoff * MS_PER_SECOND; #else ::tzset(); // Get the current time in seconds (might have DST adjustment included). time_t currentWithDST = std::time(nullptr); if (currentWithDST == static_cast<time_t>(-1)) { return 0; } // Deconstruct the time into localTime. std::tm *local = std::localtime(&currentWithDST); if (!local) { llvm_unreachable("localtime failed in localTZA()"); } long gmtoff = local->tm_gmtoff; // Use the gmtoff field and subtract an hour if currently in DST. return (gmtoff * MS_PER_SECOND) - (local->tm_isdst ? MS_PER_HOUR : 0); #endif } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.8 static const int32_t DAYS_IN_1_YEAR = 365; static const int32_t DAYS_IN_4_YEARS = DAYS_IN_1_YEAR * 4 + 1; static const int32_t DAYS_IN_100_YEARS = DAYS_IN_4_YEARS * 25 - 1; static const int32_t DAYS_IN_400_YEARS = DAYS_IN_100_YEARS * 4 + 1; // ES5.1 15.9.1.1 // The actual range of times supported by ECMAScript Date objects is slightly // smaller: exactly –100,000,000 days to 100,000,000 days measured relative to // midnight at the beginning of 01 January, 1970 UTC. static const int32_t BASE_YEAR = -274000; static const int32_t DAYS_FROM_BASE_YEAR_TO_1970 = (-BASE_YEAR / 400) * DAYS_IN_400_YEARS + 4 * DAYS_IN_400_YEARS + 3 * DAYS_IN_100_YEARS + 17 * DAYS_IN_4_YEARS + 2 * DAYS_IN_1_YEAR; /// \p year will be set to the year the \p epochDays falls in. /// \p yearAsEpochDays will be set to the number of days from 1970-01-01 to Jan /// 1st of \p year (e.g. 0 represents 1970, 365 represents 1971, /// 1096 represents 1973). /// \p dayOfYear will be set to the date the \p epochDays fall on, represented /// as number of days since Jan 1st (e.g. 0 represents Jan 1; /// 59 represents Feb 29 if \p year is a leap year, Mar 1 otherwise). static void decomposeEpochDays( int32_t epochDays, int32_t *year, int32_t *yearAsEpochDays, int32_t *dayOfYear) { *year = BASE_YEAR; *yearAsEpochDays = -DAYS_FROM_BASE_YEAR_TO_1970; *dayOfYear = epochDays + DAYS_FROM_BASE_YEAR_TO_1970; int32_t countOf400Years = *dayOfYear / DAYS_IN_400_YEARS; *year += countOf400Years * 400; *yearAsEpochDays += countOf400Years * DAYS_IN_400_YEARS; *dayOfYear -= countOf400Years * DAYS_IN_400_YEARS; int32_t countOf100Years = *dayOfYear / DAYS_IN_100_YEARS; *year += countOf100Years * 100; *yearAsEpochDays += countOf100Years * DAYS_IN_100_YEARS; *dayOfYear -= countOf100Years * DAYS_IN_100_YEARS; int32_t countOf4Years = *dayOfYear / DAYS_IN_4_YEARS; *year += countOf4Years * 4; *yearAsEpochDays += countOf4Years * DAYS_IN_4_YEARS; *dayOfYear -= countOf4Years * DAYS_IN_4_YEARS; int32_t countOf1Year = *dayOfYear / DAYS_IN_1_YEAR; *year += countOf1Year * 1; *yearAsEpochDays += countOf1Year * DAYS_IN_1_YEAR; *dayOfYear -= countOf1Year * DAYS_IN_1_YEAR; } static int32_t weekDayFromEpochDays(int32_t epochDays) { return floorMod(epochDays + 4, 7); } static int32_t epochDaysForYear2006To2033[] = { 13149, 13514, 13879, 14245, 14610, 14975, 15340, 15706, 16071, 16436, 16801, 17167, 17532, 17897, 18262, 18628, 18993, 19358, 19723, 20089, 20454, 20819, 21184, 21550, 21915, 22280, 22645, 23011}; /// Returns an equivalent year, represented as number of days since 1970-01-01, /// for the purpose of determining DST using the rules in ES5.1 15.9.1.8 /// Daylight Saving Time Adjustment. /// The returned year is guaranteed to be in range [1970, 2037]. /// \p yearAsEpochDays must be set to the number of days from 1970-01-01 to Jan /// 1st of \p year. static int32_t equivalentYearAsEpochDays( int32_t year, int32_t yearAsEpochDays) { if (year >= 1970 && year <= 2037) { // This avoids surprising results for current year and nearby years. // It also reduces overhead for the most common cases. return yearAsEpochDays; } int32_t wkDay = weekDayFromEpochDays(yearAsEpochDays); // * 2006-01-01 and 2012-01-01 are both Sundays. // * Starting 2006/2012, for the 40 years after it, there is a leap year // every 4 years, with no exceptions (i.e. 100 year rules). // This is the basis of the following two bullet points. // * any_int * 12 % 28 is guaranteed to be a multiple of 4. // As a result, the following operations does not change // whether a year is a leap year or not. // * Every 4 years, there is 1 leap year and 3 non-leap years. // (365*3+366) % 7 = 5. This is the number of extra days on top of // full weeks we get every 4 years. // * After 28 (4 * 7) years, we get (5 * 7) % 7 = 0 day of extra day. // This is why subtracting 28 years does not change whether a year // is a leap year. // * After 12 (4 * 3) years, we get (5 * 3) % 7 = 1 day of extra day. // That's why adding 12 years increments weekday by 1. int32_t eqYear = (isLeapYear(year) ? 2012 : 2006) + (wkDay * 12) % 28; // Find the year in the range 2006..2033 that is equivalent mod 28. // This is to avoid anything above year 2037. eqYear = 2006 + (eqYear - 2006) % 28; return epochDaysForYear2006To2033[eqYear - 2006]; } static const int32_t SECS_PER_DAY = 24 * 60 * 60; // Numbers are from 15.9.1.1 Time Values and Time Range static const int64_t TIME_RANGE_SECS = SECS_PER_DAY * 100000000LL; /// Returns an equivalent time for the purpose of determining DST using the /// rules in ES5.1 15.9.1.8 Daylight Saving Time Adjustment /// /// \p time_ms must be within the range specified by /// ES5.1 15.9.1.1 Time Values and Time Range. /// /// Some library calls doesn't work when the input date-time cannot be /// represented as a 32-bit non-negative number of seconds since /// 1970-01-01T00:00:00. (e.g. std::localtime on Windows) /// /// Note: not "static" so that it can be tested directly. int32_t detail::equivalentTime(int64_t epochSecs) { // The math behind this implementation is similar to the EquivalentTime // function in https://github.com/v8/v8/blob/master/src/date.h assert(epochSecs >= -TIME_RANGE_SECS && epochSecs <= TIME_RANGE_SECS); int64_t epochDays, secsOfDay; floorDivMod(epochSecs, SECS_PER_DAY, &epochDays, &secsOfDay); int32_t year, yearAsEpochDays, dayOfYear; // Narrowing of epochDays will not result in truncation decomposeEpochDays(epochDays, &year, &yearAsEpochDays, &dayOfYear); int32_t eqYearAsEpochDays = equivalentYearAsEpochDays(year, yearAsEpochDays); return (eqYearAsEpochDays + dayOfYear) * SECS_PER_DAY + secsOfDay; } double daylightSavingTA(double t) { if (!std::isfinite(t)) { return std::numeric_limits<double>::quiet_NaN(); } ::tzset(); // Convert t to seconds and get the actual time needed. const double seconds = t / MS_PER_SECOND; // If the number of seconds is higher or lower than a unix timestamp can // support, clamp it. This is not correct in all cases, but returning NaN (for // Invalid Date) breaks date construction entirely. Clamping only results in // small errors in daylight savings time. This is only a problem in systems // with a 32-bit time_t, like some Android systems. time_t local = 0; if (seconds > TIME_RANGE_SECS || seconds < -TIME_RANGE_SECS) { // Return NaN if input is outside Time Range allowed in ES5.1 return std::numeric_limits<double>::quiet_NaN(); } // This will truncate any fractional seconds, which is ok for daylight // savings time calculations. local = detail::equivalentTime(static_cast<int64_t>(seconds)); std::tm *brokenTime = std::localtime(&local); if (!brokenTime) { // Local time is invalid. return std::numeric_limits<double>::quiet_NaN(); } return brokenTime->tm_isdst ? MS_PER_HOUR : 0; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.9 /// Conversion from UTC to local time. double localTime(double t) { return t + localTZA() + daylightSavingTA(t); } /// Conversion from local time to UTC. double utcTime(double t) { double ltza = localTZA(); return t - ltza - daylightSavingTA(t - ltza); } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.10 double hourFromTime(double t) { return posfmod(std::floor(t / MS_PER_HOUR), HOURS_PER_DAY); } double minFromTime(double t) { return posfmod(std::floor(t / MS_PER_MINUTE), MINUTES_PER_HOUR); } double secFromTime(double t) { return posfmod(std::floor(t / MS_PER_SECOND), SECONDS_PER_MINUTE); } double msFromTime(double t) { return posfmod(t, MS_PER_SECOND); } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.11 double makeTime(double hour, double min, double sec, double ms) { if (!std::isfinite(hour) || !std::isfinite(min) || !std::isfinite(sec) || !std::isfinite(ms)) { return std::numeric_limits<double>::quiet_NaN(); } double h = std::trunc(hour); double m = std::trunc(min); double s = std::trunc(sec); double milli = trunc(ms); return h * MS_PER_HOUR + m * MS_PER_MINUTE + s * MS_PER_SECOND + milli; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.12 double makeDay(double year, double month, double date) { if (!std::isfinite(year) || !std::isfinite(month) || !std::isfinite(date)) { return std::numeric_limits<double>::quiet_NaN(); } double y = std::trunc(year); double m = std::trunc(month); double dt = std::trunc(date); // Actual year and month, accounting for the month being greater than 11. // Need to do this because it changes the leap year calculations. double ym = y + std::floor(m / 12); double mn = posfmod(m, 12); bool leap = isLeapYear(ym); // Day of the first day of the year ym. double yd = std::floor(timeFromYear(ym) / MS_PER_DAY); // Day of the first day of the month mn. double md = dayFromMonth(mn, leap); // Final date is the first day of the year, offset by the first day of the // month, with dt - 1 to account for the day within the month. return yd + md + dt - 1; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.13 double makeDate(double day, double t) { if (!std::isfinite(day) || !std::isfinite(t)) { return std::numeric_limits<double>::quiet_NaN(); } return day * MS_PER_DAY + t; } //===----------------------------------------------------------------------===// // ES5.1 15.9.1.14 double timeClip(double t) { if (!std::isfinite(t) || std::abs(t) > 8.64e15) { return std::numeric_limits<double>::quiet_NaN(); } // Truncate and make -0 into +0. return std::trunc(t) + 0; } //===----------------------------------------------------------------------===// // toString Functions void dateToISOString(double t, double, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; /// Make these ints here because we're printing and we have bounds on /// their values. Makes printing very easy. int32_t y = yearFromTime(t); int32_t m = monthFromTime(t) + 1; // monthFromTime(t) is 0-indexed. int32_t d = dateFromTime(t); if (y < 0 || y > 9999) { // Handle extended years. os << llvh::format("%+07d-%02d-%02d", y, m, d); } else { os << llvh::format("%04d-%02d-%02d", y, m, d); } } void timeToISOString(double t, double tza, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; /// Make all of these ints here because we're printing and we have bounds on /// their values. Makes printing very easy. int32_t h = hourFromTime(t); int32_t min = minFromTime(t); int32_t s = secFromTime(t); int32_t ms = msFromTime(t); if (tza == 0) { // Zulu time, output Z as the time zone. os << llvh::format("%02d:%02d:%02d.%03dZ", h, min, s, ms); } else { // Calculate the +HH:mm expression for the time zone adjustment. // First account for the sign, then perform calculations on positive TZA. char sign = tza >= 0 ? '+' : '-'; double tzaPos = std::abs(tza); int32_t tzh = hourFromTime(tzaPos); int32_t tzm = minFromTime(tzaPos); os << llvh::format( "%02d:%02d:%02d.%03d%c%02d:%02d", h, min, s, ms, sign, tzh, tzm); } } static void datetimeToISOString( double t, double tza, llvh::SmallVectorImpl<char> &buf, char separator) { dateToISOString(t, tza, buf); buf.push_back(separator); timeToISOString(t, tza, buf); } void datetimeToISOString( double t, double tza, llvh::SmallVectorImpl<char> &buf) { return datetimeToISOString(t, tza, buf, 'T'); } void datetimeToLocaleString(double t, llvh::SmallVectorImpl<char16_t> &buf) { return platform_unicode::dateFormat(t, true, true, buf); } void dateToLocaleString(double t, llvh::SmallVectorImpl<char16_t> &buf) { return platform_unicode::dateFormat(t, true, false, buf); } void timeToLocaleString(double t, llvh::SmallVectorImpl<char16_t> &buf) { return platform_unicode::dateFormat(t, false, true, buf); } // ES9.0 Table 46 static const char *const weekdayNames[7]{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", }; // ES9.0 Table 47 static const char *const monthNames[12]{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", }; void dateString(double t, double, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; // Make these ints here because we're printing and we have bounds on // their values. Makes printing very easy. int32_t y = yearFromTime(t); int32_t m = monthFromTime(t); // monthFromTime(t) is 0-indexed. int32_t d = dateFromTime(t); int32_t wd = weekDay(t); // 7. Return the string-concatenation of weekday, the code unit 0x0020 // (SPACE), month, the code unit 0x0020 (SPACE), day, the code unit 0x0020 // (SPACE), and year. // Example: Mon Jul 22 2019 os << llvh::format("%s %s %02d %0.4d", weekdayNames[wd], monthNames[m], d, y); } void timeString(double t, double tza, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; int32_t hour = hourFromTime(t); int32_t minute = minFromTime(t); int32_t second = secFromTime(t); // Example: 15:50:49 GMT os << llvh::format("%02d:%02d:%02d GMT", hour, minute, second); } void timeZoneString(double t, double tza, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; // We've already computed the TZA, so use that as the offset. double offset = tza; // 4. If offset >= 0, let offsetSign be "+"; otherwise, let offsetSign be "-". char offsetSign = offset >= 0 ? '+' : '-'; // 5. Let offsetMin be the String representation of MinFromTime(abs(offset)), // formatted as a two-digit decimal number, padded to the left with a zero if // necessary. int32_t offsetMin = minFromTime(std::abs(offset)); // 6. Let offsetHour be the String representation of // HourFromTime(abs(offset)), formatted as a two-digit decimal number, padded // to the left with a zero if necessary. int32_t offsetHour = hourFromTime(std::abs(offset)); // 7. Let tzName be an implementation-defined string that is either the empty // string or the string-concatenation of the code unit 0x0020 (SPACE), the // code unit 0x0028 (LEFT PARENTHESIS), an implementation-dependent timezone // name, and the code unit 0x0029 (RIGHT PARENTHESIS). // TODO: Make this something other than empty string. // 8. Return the string-concatenation of offsetSign, offsetHour, offsetMin, // and tzName. // Example: -0700 os << llvh::format("%c%02d%02d", offsetSign, offsetHour, offsetMin); } void dateTimeString(double tv, double tza, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; dateString(tv, tza, buf); // Return the string-concatenation of DateString(t), the code unit 0x0020 // (SPACE), TimeString(t), and TimeZoneString(tv). // Example: Mon Jul 22 2019 15:51:50 GMT-0700 os << " "; timeString(tv, tza, buf); timeZoneString(tv, tza, buf); } void dateTimeUTCString( double tv, double tza, llvh::SmallVectorImpl<char> &buf) { llvh::raw_svector_ostream os{buf}; // Make these ints here because we're printing and we have bounds on // their values. Makes printing very easy. int32_t y = yearFromTime(tv); int32_t m = monthFromTime(tv); // monthFromTime(t) is 0-indexed. int32_t d = dateFromTime(tv); int32_t wd = weekDay(tv); // 8. Return the string-concatenation of weekday, ",", the code unit 0x0020 // (SPACE), day, the code unit 0x0020 (SPACE), month, the code unit 0x0020 // (SPACE), year, the code unit 0x0020 (SPACE), and TimeString(tv). // Example: Mon Jul 22 2019 15:51:50 GMT os << llvh::format( "%s, %02d %s %0.4d ", weekdayNames[wd], d, monthNames[m], y); timeString(tv, tza, buf); } void timeTZString(double tv, double tza, llvh::SmallVectorImpl<char> &buf) { // Return the string-concatenation of TimeString(t) and TimeZoneString(tv). // Example: 15:51:50 GMT-0700 timeString(tv, tza, buf); timeZoneString(tv, tza, buf); } //===----------------------------------------------------------------------===// // Date parsing /// \return true if c represents a digit between 0 and 9. static inline bool isDigit(char16_t c) { return u'0' <= c && c <= u'9'; } /// \return true if c represents an alphabet letter. static inline bool isAlpha(char16_t c) { c |= 'a' ^ 'A'; // Lowercase return 'a' <= c && c <= 'z'; } /// Read a number from the iterator at \p it into \p x. /// Can read integers that consist entirely of digits. /// \param[in,out] it is modified to the new start point of the scan if /// successful. /// \param end the end of the string. /// \param[out] x modified to contain the scanned integer. /// \return true if successful, false if failed. template <class InputIter> static bool scanInt(InputIter &it, const InputIter end, int32_t &x) { llvh::SmallString<16> str{}; if (it == end) { return false; } for (; it != end && isDigit(*it); ++it) { str += static_cast<char>(*it); } llvh::StringRef ref{str}; // getAsInteger returns false to signify success. return !ref.getAsInteger(10, x); } static double parseISODate(StringView u16str) { constexpr double nan = std::numeric_limits<double>::quiet_NaN(); auto it = u16str.begin(); auto end = u16str.end(); // Used to indicate the negation multiplier on an integer. // 1 for positive, -1 for negative. double sign; // Initialize these fields to their defaults. int32_t y, m{1}, d{1}, h{0}, min{0}, s{0}, ms{0}, tzh{0}, tzm{0}; auto consume = [&](char16_t ch) { if (it != end && *it == ch) { ++it; return true; } return false; }; // Must read the year. sign = 1; if (consume(u'+')) { sign = 1; } else if (consume(u'-')) { sign = -1; } if (!scanInt(it, end, y)) { return nan; } y *= sign; if (consume(u'-')) { // Try to read the month. if (!scanInt(it, end, m)) { return nan; } if (consume(u'-')) { // Try to read the date. if (!scanInt(it, end, d)) { return nan; } } } // See if there's a time. if (consume(u'T') || consume(u' ')) { // Hours and minutes must exist. if (!scanInt(it, end, h)) { return nan; } if (!consume(u':')) { return nan; } if (!scanInt(it, end, min)) { return nan; } if (consume(u':')) { // Try to read seconds. if (!scanInt(it, end, s)) { return nan; } if (consume(u'.')) { // Try to read fraction of a second. if (it == end || !isDigit(*it)) { return nan; } // Position of the milliseconds counter. // Start at the 100s place and discard anything after the third digit by // dividing by 10 every iteration. int32_t pos = 100; for (; it != end && isDigit(*it); ++it) { ms += pos * (*it - '0'); pos /= 10; } } } if (it == end) { // ES12 21.4.3.2: When the UTC offset representation is absent, date-only // forms are interpreted as a UTC time and date-time forms are interpreted // as a local time. double t = makeDate(makeDay(y, m - 1, d), makeTime(h, min, s, ms)); t = utcTime(t); return t; } // Try to parse a timezone. if (consume(u'Z')) { tzh = 0; tzm = 0; } else { // Try to parse the fully specified timezone: [+/-]HH:mm. if (consume(u'+')) { sign = 1; } else if (consume(u'-')) { sign = -1; } else { // Need a + or a -. return nan; } if (it > end - 2) { return nan; } if (!scanInt(it, it + 2, tzh)) { return nan; } tzh *= sign; consume(u':'); if (it > end - 2) { return nan; } if (!scanInt(it, it + 2, tzm)) { return nan; } tzm *= sign; } } if (it != end) { // Should be done parsing. return nan; } // Account for the fact that m was 1-indexed and the timezone offset. return makeDate(makeDay(y, m - 1, d), makeTime(h - tzh, min - tzm, s, ms)); } static double parseESDate(StringView str) { constexpr double nan = std::numeric_limits<double>::quiet_NaN(); StringView tok = str; // Initialize these fields to their defaults. int32_t y, m{1}, d{1}, h{0}, min{0}, s{0}, ms{0}, tzh{0}, tzm{0}; double sign = 1; // Example strings to parse: // Mon Jul 15 2019 14:33:22 GMT-0700 (PDT) // Mon, 15 Jul 2019 14:33:22 GMT // The comma, time zone adjustment, and description are optional, // Current index we are parsing. auto it = str.begin(); auto end = str.end(); /// Read a string starting at `it` into `tok`. /// \p len the number of characters to scan in the string. /// \return true if successful, false if failed. auto scanStr = [&str, &tok, &it](int32_t len) -> bool { if (it + len > str.end()) { return false; } tok = str.slice(it, it + len); it += len; return true; }; /// Reads the next \p len characters into `tok`, /// but instead of consuming \p len chars, it consumes a single word /// whatever how long it is (i.e. until a space is encountered). /// e.g. /// &str ="Garbage G MayG" /// scanStrAndSkipWord(3); consumeSpaces(); // &str="G MayG", &tok="Gar" /// scanStrAndSkipWord(3); consumeSpaces(); // &str="MayG" , &tok="G M" /// scanStrAndSkipWord(3); consumeSpaces(); // &str="" , &tok="May" /// scanStrAndSkipWord(3); // -> false /// \return true if successful, false if failed. auto scanStrAndSkipWord = [&str, &tok, &it](int32_t len) -> bool { if (it + len > str.end()) return false; tok = str.slice(it, it + len); while (it != str.end() && !std::isspace(*it)) it++; return true; }; auto consume = [&](char16_t ch) { if (it != str.end() && *it == ch) { ++it; return true; } return false; }; auto consumeSpaces = [&]() { while (it != str.end() && std::isspace(*it)) ++it; }; // Weekday if (!scanStr(3)) return nan; bool foundWeekday = false; for (const char *name : weekdayNames) { if (tok.equals(llvh::arrayRefFromStringRef(name))) { foundWeekday = true; break; } } if (!foundWeekday) return nan; /// If we found a valid Month string from the current `tok`. auto tokIsMonth = [&]() -> bool { for (uint32_t i = 0; i < sizeof(monthNames) / sizeof(monthNames[0]); ++i) { if (tok.equals(llvh::arrayRefFromStringRef(monthNames[i]))) { // m is 1-indexed. m = i + 1; return true; } } return false; }; // Day Month Year // or // Month Day Year while (it != str.end()) { if (isDigit(*it)) { // Day scanInt(it, end, d); // Month consumeSpaces(); // e.g. `Janwhatever` will get read as `Jan` if (!scanStrAndSkipWord(3)) return nan; // `tok` is now set to the Month candidate. if (!tokIsMonth()) return nan; break; } if (isAlpha(*it)) { // try Month if (!scanStrAndSkipWord(3)) return nan; // `tok` is now set to the Month candidate. if (tokIsMonth()) { // Day consumeSpaces(); if (!scanInt(it, end, d)) return nan; break; } // Continue scanning for Month. continue; } // Ignore any garbage. ++it; } // Year consumeSpaces(); if (!scanInt(it, end, y)) return nan; // Hour:minute:second. consumeSpaces(); if (it != end) { if (!scanInt(it, end, h)) return nan; if (!consume(':')) return nan; if (!scanInt(it, end, min)) return nan; if (!consume(':')) return nan; if (!scanInt(it, end, s)) return nan; } // Space and time zone. consumeSpaces(); if (it == end) { // Default to local time zone if no time zone provided double t = makeDate(makeDay(y, m - 1, d), makeTime(h, min, s, ms)); t = utcTime(t); return t; } struct KnownTZ { const char *tz; int32_t tzh; }; // Known time zones per RFC 2822. // All other obsolete time zones that aren't in this array treated as +00:00. static constexpr KnownTZ knownTZs[] = { {"GMT", 0}, {"EDT", -4}, {"EST", -5}, {"CDT", -5}, {"CST", -6}, {"MDT", -6}, {"MST", -7}, {"PDT", -7}, {"PST", -8}, }; // TZ name is optional, but if there is a letter, it is the only option. if ('A' <= *it && *it <= 'Z') { if (!scanStr(3)) return nan; for (const KnownTZ &knownTZ : knownTZs) { if (tok.equals(llvh::arrayRefFromStringRef(knownTZ.tz))) { tzh = knownTZ.tzh; break; } } } if (it == end) goto complete; // Prevent "CDT+0700", for example. if (tzh != 0 && it != end) return nan; // Sign of the timezone adjustment. if (consume('+')) sign = 1; else if (consume('-')) sign = -1; else return nan; // Hour and minute of timezone adjustment. if (it > end - 4) return nan; if (!scanInt(it, it + 2, tzh)) return nan; tzh *= sign; if (!scanInt(it, it + 2, tzm)) return nan; tzm *= sign; if (it != end) { // Optional parenthesized description of timezone (must be at the end). if (!consume(' ')) return nan; if (!consume('(')) return nan; while (it != end && *it != ')') ++it; if (!consume(')')) return nan; } if (it != end) return nan; complete: // Account for the fact that m was 1-indexed and the timezone offset. return makeDate(makeDay(y, m - 1, d), makeTime(h - tzh, min - tzm, s, ms)); } double parseDate(StringView str) { double result = parseISODate(str); if (!std::isnan(result)) { return result; } return parseESDate(str); } } // namespace vm } // namespace hermes