From ed3f239d5360ff76a95df7574779358e868c5e8c Mon Sep 17 00:00:00 2001 From: "Gang Zhao (Hermes)" Date: Mon, 24 Jun 2024 15:15:09 -0700 Subject: [PATCH] Introduce LocalTimeOffsetCache (#1446) Summary: Pull Request resolved: https://github.com/facebook/hermes/pull/1446 LocalTimeOffsetCache supports caching time zone standard offset and DST offset for time conversion between UTC and local. The caching mechanism is mostly the same as V8 (when ICU is not enabled). Reviewed By: dannysu Differential Revision: D52153598 --- include/hermes/VM/JSLib/DateCache.h | 211 +++++++++++++++++++ include/hermes/VM/JSLib/DateUtil.h | 4 + include/hermes/VM/JSLib/JSLibStorage.h | 5 + lib/VM/CMakeLists.txt | 1 + lib/VM/JSLib/DateCache.cpp | 281 +++++++++++++++++++++++++ lib/VM/JSLib/DateUtil.cpp | 8 +- 6 files changed, 504 insertions(+), 6 deletions(-) create mode 100644 include/hermes/VM/JSLib/DateCache.h create mode 100644 lib/VM/JSLib/DateCache.cpp diff --git a/include/hermes/VM/JSLib/DateCache.h b/include/hermes/VM/JSLib/DateCache.h new file mode 100644 index 00000000000..84fefc90a73 --- /dev/null +++ b/include/hermes/VM/JSLib/DateCache.h @@ -0,0 +1,211 @@ +/* + * 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. + */ + +#ifndef HERMES_VM_JSLIB_DATECACHE_H +#define HERMES_VM_JSLIB_DATECACHE_H + +#include "hermes/VM/JSLib/DateUtil.h" + +namespace hermes { +namespace vm { + +/// Type of time represented by an epoch. +enum class TimeType : int8_t { + Local, + Utc, +}; + +/// Cache local time offset (including daylight saving offset). +/// +/// Standard offset is computed via localTZA() and cached. DST is cached in an +/// array of time intervals, time in the same interval has a fixed DST offset. +/// For every new time point that is not included in any existing time interval, +/// compute its DST offset, try extending an existing interval, or creating a +/// new one and storing to the cache array (replace the least recently used +/// cache if it's full). +/// +/// The algorithm of the DST caching is described below: +/// 1. Initialize every interval in the DST cache array to be [t_max, -t_max], +/// which is considered an "empty" interval, here t_max is the maximum epoch +/// time supported by some OS time APIs. Initialize candidate_ to point to the +/// first interval in the array. And constant kDSTDeltaMs is the maximum +/// time range that can have at most one DST transition. Each entry also has an +/// epoch value that is used to find out the least recently used entry, but we +/// omit the operations on them in this description. +/// 2. Given a UTC time point t, check if it's included in candidate_, if yes, +/// return the DST offset of candidate_. Otherwise, go to step 3. +/// 3. Search the cache array and try to find two intervals: +/// before: [s_before, e_before] where s_before <= t and as close as +/// possible. +// after: [s_after, e_after] where t < s_after and e_after ends as early as +/// possible. +/// If one interval is not found, let it point to an empty interval in the +/// cache array (if no empty interval in it, reset the least recently used +/// one to empty and use it). Assign before to candidate_. +/// 4. Check if the the before interval is empty, if yes, compute the DST of t, +/// and set its interval to [t, t]. Otherwise, go to step 5. +/// 5. Check if t is included in the new non-empty before interval, if yes, +/// return its DST. Otherwise, go to step 6. +/// 6. If t > R_new, where R_new = before.end + kDSTDeltaMs, compute the DST +/// offset for t and call extendOrRecomputeCacheEntry(after, t, offset) (this +/// function is described later) to update after, assign after to candidate_, +/// return offset. Otherwise, go to step 7. +/// 7. If t <= R_new < after.start, compute the DST offset for t and call +/// extendOrRecomputeCacheEntry(after, t, offset) to update after. +/// 8. If before.offset == after.offset, merge after to before and +/// reset after, then return the offset. Otherwise, go to step 9. +/// 9. At this step, there must be one DST transition in interval +/// (before.end, after.start], compute DST of t and do binary search to +/// find a time point that has the same offset as t, and extend before or +/// after to it. In the end, return the offset (if after is hit, assign it +/// to candidate_). +/// 10. Given a new UTC time point, repeat step 2 - 9. +/// +/// Algorithm for extendOrRecomputeAfterCache(entry, t, offset): +/// 1. If offset == entry.offset and entry.start-kDSTDeltaMs <= t <= entry.end, +/// let entry.start = t and return. +/// 2. If entry is not empty, scan every entry in the cache array (except +/// candidate_), find out the least recently used one, reset it to empty. +/// 3. Assign offset to entry and update its interval to be [t, t]. +/// +/// Note that on Linux, the standard offset and DST offset is unchanged even if +/// TZ is updated, since the underlying time API in C library caches the time +/// zone. On Windows, currently we don't detect TZ changes as well. But this +/// could change if we migrate the usage of C API to Win32 API. On MacOS, the +/// time API does not cache, so we will check if the standard offset has changed +/// in computeDaylightSaving(), and reset both the standard offset cache and DST +/// cache in the next call to getLocalTimeOffset() or daylightSavingOffsetInMs() +/// (the current call will still use the old TZ). This is to ensure that they +/// are consistent w.r.t. the current TZ. +class LocalTimeOffsetCache { + public: + /// All runtime functionality should use the instance provided in + /// JSLibStorage, this is public only because we need to construct a instance + /// in the unit tests (alternatively, we have to create a singleton function + /// to be used in the tests). + LocalTimeOffsetCache() { + reset(); + } + + /// Reset the standard local time offset and the DST cache. + void reset() { + ::tzset(); + ltza_ = localTZA(); + caches_.fill(DSTCacheEntry{}); + candidate_ = caches_.data(); + epoch_ = 0; + needsToReset_ = false; + } + + /// Compute local timezone offset (DST included). + /// \param timeMs time in milliseconds. + /// \param timeType whether \p timeMs is UTC or local time. + double getLocalTimeOffset(double timeMs, TimeType timeType); + + /// \param utcTimeMs UTC epoch in milliseconds. + /// \return Daylight saving offset at time \p utcTimeMs. + int daylightSavingOffsetInMs(int64_t utcTimeMs); + + private: + LocalTimeOffsetCache(const LocalTimeOffsetCache &) = delete; + LocalTimeOffsetCache operator=(const LocalTimeOffsetCache &) = delete; + + /// Number of cached time intervals for DST. + static constexpr unsigned kCacheSize = 32; + /// Default length of each time interval. The implementation relies on the + /// fact that no time zones have more than one daylight savings offset change + /// per 19 days. In Egypt in 2010 they decided to suspend DST during Ramadan. + /// This led to a short interval where DST is in effect from September 10 to + /// September 30. + static constexpr int64_t kDSTDeltaMs = 19 * SECONDS_PER_DAY * MS_PER_SECOND; + // The largest time that can be passed to OS date-time library functions. + static constexpr int64_t kMaxEpochTimeInMs = + std::numeric_limits::max() * MS_PER_SECOND; + + struct DSTCacheEntry { + /// Start and end time of this DST cache interval, in UTC time. + int64_t startMs{kMaxEpochTimeInMs}; + int64_t endMs{-kMaxEpochTimeInMs}; + /// The DST offset in [startMs, endMs]. + int dstOffsetMs{0}; + /// Used for LRU. + int epoch{0}; + + /// \return whether this is a valid interval. + bool isEmpty() const { + return startMs > endMs; + } + + /// \return True if \p utcTimeMs is included in this interval. + bool include(int64_t utcTimeMs) const { + return startMs <= utcTimeMs && utcTimeMs <= endMs; + } + }; + + /// Compute the DST offset at UTC time \p timeMs. + /// Note that this may update needsToReset_ if it detects a different + /// standard offset than the cached one. + int computeDaylightSaving(int64_t utcTimeMs); + + /// Increase the epoch counter and return it. + int bumpEpoch() { + ++epoch_; + return epoch_; + } + + /// Find the least recently used DST cache and reuse it. + /// \param skip do not scan this cache. Technically, it can be nullptr, which + /// means we don't skip any entry in the cache array. But currently we always + /// passed in an meaningful entry pointer (e.g., candidate_). + /// \return an empty DST cache. This should never return nullptr. + DSTCacheEntry *leastRecentlyUsedExcept(const DSTCacheEntry *const skip); + + /// Scan the DST caches to find a cached interval starts at or before \p + /// timeMs (as late as possible) and an interval ends after \p timeMs (as + /// early as possible). If none is found, reuse an empty cache. Return the + /// found two cache entries. + std::tuple findBeforeAndAfterEntries( + int64_t timeMs); + + /// If entry->dstOffsetMs == \p dstOffsetMs and \p timeMs is included in + /// [entry->startMs - kDSTDeltaMs, entry->endMs], extend entry to + /// [timeMs, entry->endMs]. + /// Otherwise, let \p entry point to the least recently used cache entry + /// (except the candidate_ cache) and update its interval to be [timeMs, + /// timeMs], and its DST offset to be \p dstOffsetMs. + void extendOrRecomputeCacheEntry( + DSTCacheEntry *&entry, + int64_t timeMs, + int dstOffsetMs); + + /// Integer counter used to find least recently used cache. + int epoch_; + /// Point to one entry in the caches_ array, and this invariant always + /// holds: candidate_->startMs <= t <= candidate_->endMs, where t is the last + /// seen time point. It likely will cover subsequent time points. + DSTCacheEntry *candidate_; + /// A list of cached intervals computed for previously seen time points. + /// In the beginning, each interval is initialized to empty. When every + /// interval is non-empty and we need a new empty one, reset the least + /// recently used one (by comparing the epoch value) to empty. + std::array caches_; + /// The standard local timezone offset (without DST offset). + int64_t ltza_; + /// Whether needs to reset the cache and ltza_. + /// We don't do reset in the middle of daylightSavingOffsetInMs() (essentially + /// before any call to computeDaylightSaving() that will detect TZ changes) + /// because it may cause that function never return if another thread is + /// keeping updating TZ. But that means we may return incorrect result before + /// reset(). This is consistent with previous implementation of utcTime() and + /// localTime(). + bool needsToReset_; +}; + +} // namespace vm +} // namespace hermes + +#endif // HERMES_VM_JSLIB_DATECACHE_H diff --git a/include/hermes/VM/JSLib/DateUtil.h b/include/hermes/VM/JSLib/DateUtil.h index 0807e7deb24..40a190abbad 100644 --- a/include/hermes/VM/JSLib/DateUtil.h +++ b/include/hermes/VM/JSLib/DateUtil.h @@ -41,6 +41,10 @@ constexpr double MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE; constexpr double MS_PER_HOUR = MS_PER_MINUTE * MINUTES_PER_HOUR; constexpr double MS_PER_DAY = MS_PER_HOUR * HOURS_PER_DAY; +// Time value ranges from ES2024 21.4.1.1. +constexpr int64_t TIME_RANGE_SECS = SECONDS_PER_DAY * 100000000LL; +constexpr int64_t TIME_RANGE_MS = TIME_RANGE_SECS * MS_PER_SECOND; + //===----------------------------------------------------------------------===// // Current time diff --git a/include/hermes/VM/JSLib/JSLibStorage.h b/include/hermes/VM/JSLib/JSLibStorage.h index 0f02084de67..0bdd5173066 100644 --- a/include/hermes/VM/JSLib/JSLibStorage.h +++ b/include/hermes/VM/JSLib/JSLibStorage.h @@ -8,6 +8,8 @@ #ifndef HERMES_VM_JSLIB_RUNTIMECOMMONSTORAGE_H #define HERMES_VM_JSLIB_RUNTIMECOMMONSTORAGE_H +#include "hermes/VM/JSLib/DateCache.h" + #include namespace hermes { @@ -26,6 +28,9 @@ struct JSLibStorage { /// PRNG used by Math.random() std::mt19937_64 randomEngine_; bool randomEngineSeeded_ = false; + + /// Time zone offset cache used in conversion between UTC and local time. + LocalTimeOffsetCache localTimeOffsetCache; }; } // namespace vm diff --git a/lib/VM/CMakeLists.txt b/lib/VM/CMakeLists.txt index 61c9cf3f22a..c3dbc596e0b 100644 --- a/lib/VM/CMakeLists.txt +++ b/lib/VM/CMakeLists.txt @@ -113,6 +113,7 @@ set(source_files JSLib/RegExp.cpp JSLib/RegExpStringIterator.cpp JSLib/DateUtil.cpp + JSLib/DateCache.cpp JSLib/Sorting.cpp JSLib/Symbol.cpp JSLib/Date.cpp JSLib/DateUtil.cpp diff --git a/lib/VM/JSLib/DateCache.cpp b/lib/VM/JSLib/DateCache.cpp new file mode 100644 index 00000000000..96eb1be5efc --- /dev/null +++ b/lib/VM/JSLib/DateCache.cpp @@ -0,0 +1,281 @@ +/* + * 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/DateCache.h" + +namespace hermes { +namespace vm { + +double LocalTimeOffsetCache::getLocalTimeOffset( + double timeMs, + TimeType timeType) { + if (needsToReset_) { + reset(); + } + + if (timeType == TimeType::Utc) { + // Out of allowed range by the spec, return NaN. + if (timeMs < -TIME_RANGE_MS || timeMs > TIME_RANGE_MS) + return std::numeric_limits::quiet_NaN(); + + return ltza_ + daylightSavingOffsetInMs(timeMs); + } + // To compute the DST offset, we need to use UTC time (as required by + // daylightSavingOffsetInMs()). However, getting the exact UTC time is not + // possible since that would be circular. Therefore, we approximate the UTC + // time by subtracting the standard time adjustment and then subtracting an + // additional hour to comply with the spec's requirements + // (https://tc39.es/ecma262/#sec-utc-t). + // + // For example, imagine a transition to DST that goes from UTC+0 to UTC+1, + // moving 00:00 to 01:00. Any time in the skipped hour gets mapped to a + // UTC time before the transition when we subtract an hour (e.g., 00:30 -> + // 23:30), which will correctly result in DST not being in effect. + // + // Similarly, during a transition from DST back to standard time, the hour + // from 00:00 to 01:00 is repeated. A local time in the repeated hour + // similarly gets mapped to a UTC time before the transition. + // + // Note that this will not work if the timezone offset has historical/future + // changes (which generates a different ltza than the one obtained here). + double guessUTC = timeMs - ltza_ - MS_PER_HOUR; + if (guessUTC < -TIME_RANGE_MS || guessUTC > TIME_RANGE_MS) + return std::numeric_limits::quiet_NaN(); + return ltza_ + daylightSavingOffsetInMs(guessUTC); +} + +int LocalTimeOffsetCache::computeDaylightSaving(int64_t utcTimeMs) { + std::time_t t = utcTimeMs / MS_PER_SECOND; + std::tm tm; + int ltza; +#ifdef _WINDOWS + auto err = ::localtime_s(&tm, &t); + if (err) { + return 0; + } + // It's not officially documented that whether Windows C API caches time zone, + // but actual testing shows it does. So for now, we don't detect TZ changes + // and reset the cache here. Otherwise, we have to call tzset() and + // _get_timezone(), which is thread unsafe. And this behavior is the same as + // on Linux. +#else + std::tm *brokenTime = ::localtime_r(&t, &tm); + if (!brokenTime) { + return 0; + } + int dstOffset = tm.tm_isdst ? MS_PER_HOUR : 0; + ltza = tm.tm_gmtoff * MS_PER_SECOND - dstOffset; + // If ltza changes, we need to reset the cache. + if (ltza != ltza_) { + needsToReset_ = true; + } +#endif + return tm.tm_isdst ? MS_PER_HOUR : 0; +} + +int LocalTimeOffsetCache::daylightSavingOffsetInMs(int64_t utcTimeMs) { + if (needsToReset_) { + reset(); + } + + // Some OS library calls don't work right for dates that cannot be represented + // with int32_t. ES5.1 requires to map the time to a year with same + // leap-year-ness and same starting day for the year. But for compatibility, + // other engines, such as V8, use the actual year if it is in the range of + // 1970..2037, which corresponds to the time range 0..kMaxEpochTimeInMs. + if (utcTimeMs < 0 || utcTimeMs > kMaxEpochTimeInMs) { + utcTimeMs = + detail::equivalentTime(utcTimeMs / MS_PER_SECOND) * MS_PER_SECOND; + } + + // Reset the counter to avoid overflow. Each call of this function may + // increase epoch_ by more than 1 (conservatively smaller than 10), so we need + // to subtract it from max value. In practice, this won't happen frequently + // since most time we should see cache hit. + if (LLVM_UNLIKELY( + epoch_ >= std::numeric_limits::max() - 10)) { + reset(); + } + + // Cache hit. + if (candidate_->include(utcTimeMs)) { + candidate_->epoch = bumpEpoch(); + return candidate_->dstOffsetMs; + } + + // Try to find cached intervals that happen before/after utcTimeMs. + auto [before, after] = findBeforeAndAfterEntries(utcTimeMs); + // Set candidate_ to before by default, and reassign it in case that the + // after cache is hit or used. + candidate_ = before; + + // No cached interval yet, compute a new one with utcTimeMs. + if (before->isEmpty()) { + int dstOffset = computeDaylightSaving(utcTimeMs); + before->dstOffsetMs = dstOffset; + before->startMs = utcTimeMs; + before->endMs = utcTimeMs; + before->epoch = bumpEpoch(); + return dstOffset; + } + + // Hits in the cached interval. + if (before->include(utcTimeMs)) { + before->epoch = bumpEpoch(); + return before->dstOffsetMs; + } + + // If utcTimeMs is larger than before->endMs + kDSTDeltaMs, we can't safely + // extend before, because it could have more than one DST transition in the + // interval. Instead, try if we can extend after (or recompute it). + if ((utcTimeMs - kDSTDeltaMs) > before->endMs) { + int dstOffset = computeDaylightSaving(utcTimeMs); + extendOrRecomputeCacheEntry(after, utcTimeMs, dstOffset); + // May help cache hit in subsequent calls (in case that the passed in time + // values are adjacent). + candidate_ = after; + return dstOffset; + } + + // Now, utcTimeMs is in the range of (before->endMs, before->endMs + + // kDSTDeltaMs]. + + before->epoch = bumpEpoch(); + int64_t newAfterStart = before->endMs < kMaxEpochTimeInMs - kDSTDeltaMs + ? before->endMs + kDSTDeltaMs + : kMaxEpochTimeInMs; + // If after starts too late, extend it to newAfterStart or recompute it. + if (newAfterStart < after->startMs) { + int dstOffset = computeDaylightSaving(newAfterStart); + extendOrRecomputeCacheEntry(after, newAfterStart, dstOffset); + } else { + after->epoch = bumpEpoch(); + } + + // Now after->startMs is in (before->endMs, before->endMs + kDSTDeltaMs]. + + // If before and after have the same DST offset, merge them. + if (before->dstOffsetMs == after->dstOffsetMs) { + before->endMs = after->endMs; + *after = DSTCacheEntry{}; + return before->dstOffsetMs; + } + + // Binary search in (before->endMs, after->startMs] for DST transition + // point. Note that after->startMs could be smaller than before->endMs + // + kDSTDeltaMs, but that small interval has the same DST offset, so we + // can ignore them in the below search. + // Though 5 iterations should be enough to cover kDSTDeltaMs, if the + // assumption of only one transition in kDSTDeltaMs no longer holds, we may + // not be able to search the result. We'll stop the loop after 5 iterations + // anyway. + for (int i = 4; i >= 0; --i) { + int delta = after->startMs - before->endMs; + int64_t middle = before->endMs + delta / 2; + int middleDstOffset = computeDaylightSaving(middle); + if (before->dstOffsetMs == middleDstOffset) { + before->endMs = middle; + if (utcTimeMs <= before->endMs) { + return middleDstOffset; + } + } else { + assert(after->dstOffsetMs == middleDstOffset); + after->startMs = middle; + if (utcTimeMs >= after->startMs) { + // May help cache hit in subsequent calls (in case that the passed in + // time values are adjacent). + candidate_ = after; + return middleDstOffset; + } + } + } + + // Fallthrough path of the binary search, just compute the DST for utcTimeMs. + return computeDaylightSaving(utcTimeMs); +} + +LocalTimeOffsetCache::DSTCacheEntry * +LocalTimeOffsetCache::leastRecentlyUsedExcept(const DSTCacheEntry *const skip) { + DSTCacheEntry *result = nullptr; + for (auto &cache : caches_) { + if (&cache == skip) + continue; + if (!result || result->epoch > cache.epoch) + result = &cache; + } + *result = DSTCacheEntry{}; + return result; +} + +std::tuple< + LocalTimeOffsetCache::DSTCacheEntry *, + LocalTimeOffsetCache::DSTCacheEntry *> +LocalTimeOffsetCache::findBeforeAndAfterEntries(int64_t timeMs) { + LocalTimeOffsetCache::DSTCacheEntry *before = nullptr; + LocalTimeOffsetCache::DSTCacheEntry *after = nullptr; + + // `before` should start as late as possible, while `after` should end as + // early as possible so that they're closest to timeMs. + for (auto &cache : caches_) { + if (cache.startMs <= timeMs) { + if (!before || before->startMs < cache.startMs) + before = &cache; + } else if (timeMs < cache.endMs) { + if (!after || after->endMs > cache.endMs) + after = &cache; + } + } + + // None is found, reuse an empty cache for later computation. + if (!before) { + before = leastRecentlyUsedExcept(after); + } + if (!after) { + after = leastRecentlyUsedExcept(before); + } + + assert( + before && after && before != after && + "`before` and `after` interval should be valid"); + assert( + before->isEmpty() || + before->startMs <= timeMs && + "the start time of `before` must start on or before timeMs"); + assert( + after->isEmpty() || + timeMs < after->startMs && + "The start time of `after` must start after timeMs"); + assert( + before->isEmpty() || after->isEmpty() || + before->endMs < after->startMs && + "`before` interval must strictly start before `after` interval"); + + return {before, after}; +} + +void LocalTimeOffsetCache::extendOrRecomputeCacheEntry( + DSTCacheEntry *&entry, + int64_t timeMs, + int dstOffsetMs) { + // It's safe to extend the interval if timeMs is in the checked range. + if (entry->dstOffsetMs == dstOffsetMs && + entry->startMs - kDSTDeltaMs <= timeMs && timeMs <= entry->endMs) { + entry->startMs = timeMs; + } else { + // Recompute the after cache using timeMs. + if (!entry->isEmpty()) { + entry = leastRecentlyUsedExcept(candidate_); + } + entry->startMs = timeMs; + entry->endMs = timeMs; + entry->dstOffsetMs = dstOffsetMs; + entry->epoch = bumpEpoch(); + } +} + +} // namespace vm +} // namespace hermes diff --git a/lib/VM/JSLib/DateUtil.cpp b/lib/VM/JSLib/DateUtil.cpp index ad7a08dfc0f..6f2441f146d 100644 --- a/lib/VM/JSLib/DateUtil.cpp +++ b/lib/VM/JSLib/DateUtil.cpp @@ -347,10 +347,6 @@ static int32_t equivalentYearAsEpochDays( 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 /// @@ -367,12 +363,12 @@ int32_t detail::equivalentTime(int64_t epochSecs) { // 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); + floorDivMod(epochSecs, SECONDS_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; + return (eqYearAsEpochDays + dayOfYear) * SECONDS_PER_DAY + secsOfDay; } double daylightSavingTA(double t) {