JSDateMath.cpp   [plain text]


/*
 * Copyright (C) 1999-2000 Harri Porten (porten@kde.org)
 * Copyright (C) 2006-2020 Apple Inc. All rights reserved.
 * Copyright (C) 2009 Google Inc. All rights reserved.
 * Copyright (C) 2007-2009 Torch Mobile, Inc.
 * Copyright (C) 2010 &yet, LLC. (nate@andyet.net)
 *
 * The Original Code is Mozilla Communicator client code, released
 * March 31, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Alternatively, the contents of this file may be used under the terms
 * of either the Mozilla Public License Version 1.1, found at
 * http://www.mozilla.org/MPL/ (the "MPL") or the GNU General Public
 * License Version 2.0, found at http://www.fsf.org/copyleft/gpl.html
 * (the "GPL"), in which case the provisions of the MPL or the GPL are
 * applicable instead of those above.  If you wish to allow use of your
 * version of this file only under the terms of one of those two
 * licenses (the MPL or the GPL) and not to allow others to use your
 * version of this file under the LGPL, indicate your decision by
 * deletingthe provisions above and replace them with the notice and
 * other provisions required by the MPL or the GPL, as the case may be.
 * If you do not delete the provisions above, a recipient may use your
 * version of this file under any of the LGPL, the MPL or the GPL.

 * Copyright 2006-2008 the V8 project authors. All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 *       copyright notice, this list of conditions and the following
 *       disclaimer in the documentation and/or other materials provided
 *       with the distribution.
 *     * Neither the name of Google Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "JSDateMath.h"

#include "ExceptionHelpers.h"
#include "VM.h"
#include <limits>

// icu::TimeZone and icu::BasicTimeZone features are only available in ICU C++ APIs.
// We use these C++ APIs as an exception.
#undef U_SHOW_CPLUSPLUS_API
#define U_SHOW_CPLUSPLUS_API 1
#include <unicode/basictz.h>
#include <unicode/timezone.h>
#include <unicode/unistr.h>
#undef U_SHOW_CPLUSPLUS_API
#define U_SHOW_CPLUSPLUS_API 0

namespace JSC {

void OpaqueICUTimeZoneDeleter::operator()(OpaqueICUTimeZone* timeZone)
{
    if (timeZone)
        delete bitwise_cast<icu::TimeZone*>(timeZone);
}

// Get the combined UTC + DST offset for the time passed in.
//
// NOTE: The implementation relies on the fact that no time zones have
// more than one daylight savings offset change per month.
// If this function is called with NaN it returns NaN.
LocalTimeOffset DateCache::calculateLocalTimeOffset(double millisecondsFromEpoch, WTF::TimeType inputTimeType)
{
    auto& timeZoneCache = *bitwise_cast<icu::TimeZone*>(this->timeZoneCache());
    int32_t rawOffset = 0;
    int32_t dstOffset = 0;
    UErrorCode status = U_ZERO_ERROR;
    if (inputTimeType != WTF::LocalTime) {
        constexpr bool isLocalTime = false;
        timeZoneCache.getOffset(millisecondsFromEpoch, isLocalTime, rawOffset, dstOffset, status);
    } else {
        // icu::TimeZone is a timezone instance which inherits icu::BasicTimeZone.
        // https://unicode-org.atlassian.net/browse/ICU-13705 will move getOffsetFromLocal to icu::TimeZone.
        static_cast<const icu::BasicTimeZone&>(timeZoneCache).getOffsetFromLocal(millisecondsFromEpoch, icu::BasicTimeZone::kFormer, icu::BasicTimeZone::kFormer, rawOffset, dstOffset, status);
    }
    // The above can fail if input date is invalid: NaN etc.
    // We can return any values in this case since later we fail when computing non timezone offset part anyway.
    if (U_FAILURE(status))
        return { false, 0 };
    return { !!dstOffset, rawOffset + dstOffset };
}

LocalTimeOffset DateCache::localTimeOffset(double millisecondsFromEpoch, WTF::TimeType inputTimeType)
{
    LocalTimeOffsetCache& cache = inputTimeType == WTF::LocalTime ? m_localTimeOffsetCache : m_utcTimeOffsetCache;

    double start = cache.start;
    double end = cache.end;

    auto resetCache = [&]() {
        // Compute the DST offset for the time and shrink the cache interval
        // to only contain the time. This allows fast repeated DST offset
        // computations for the same time.
        LocalTimeOffset offset = calculateLocalTimeOffset(millisecondsFromEpoch, inputTimeType);
        cache.offset = offset;
        cache.start = millisecondsFromEpoch;
        cache.end = millisecondsFromEpoch;
        cache.incrementStart = WTF::msPerMonth;
        cache.incrementEnd = WTF::msPerMonth;
        return offset;
    };

    // If the time fits in the cached interval, return the cached offset.
    if (start <= millisecondsFromEpoch && millisecondsFromEpoch <= end)
        return cache.offset;

    if (start <= millisecondsFromEpoch) {
        // Compute a possible new interval end.
        double newEnd = end + cache.incrementEnd;
        if (!(millisecondsFromEpoch <= newEnd))
            return resetCache();

        LocalTimeOffset endOffset = calculateLocalTimeOffset(newEnd, inputTimeType);
        if (cache.offset == endOffset) {
            // If the offset at the end of the new interval still matches
            // the offset in the cache, we grow the cached time interval
            // and return the offset.
            cache.end = newEnd;
            cache.incrementStart = WTF::msPerMonth;
            cache.incrementEnd = WTF::msPerMonth;
            return endOffset;
        }
        LocalTimeOffset offset = calculateLocalTimeOffset(millisecondsFromEpoch, inputTimeType);
        if (offset == endOffset) {
            // The offset at the given time is equal to the offset at the
            // new end of the interval, so that means that we've just skipped
            // the point in time where the DST offset change occurred. Update
            // the interval to reflect this and reset the increment.
            cache.start = millisecondsFromEpoch;
            cache.end = newEnd;
            cache.incrementStart = WTF::msPerMonth;
            cache.incrementEnd = WTF::msPerMonth;
        } else {
            // The interval contains a DST offset change and the given time is
            // before it. Adjust the increment to avoid a linear search for
            // the offset change point and change the end of the interval.
            cache.incrementEnd /= 3;
            cache.end = millisecondsFromEpoch;
        }
        // Update the offset in the cache and return it.
        cache.offset = offset;
        return offset;
    }

    // Compute a possible new interval start.
    double newStart = start - cache.incrementStart;
    if (!(newStart <= millisecondsFromEpoch))
        return resetCache();

    LocalTimeOffset startOffset = calculateLocalTimeOffset(newStart, inputTimeType);
    if (cache.offset == startOffset) {
        // If the offset at the start of the new interval still matches
        // the offset in the cache, we grow the cached time interval
        // and return the offset.
        cache.start = newStart;
        cache.incrementStart = WTF::msPerMonth;
        cache.incrementEnd = WTF::msPerMonth;
        return startOffset;
    }
    LocalTimeOffset offset = calculateLocalTimeOffset(millisecondsFromEpoch, inputTimeType);
    if (offset == startOffset) {
        // The offset at the given time is equal to the offset at the
        // new start of the interval, so that means that we've just skipped
        // the point in time where the DST offset change occurred. Update
        // the interval to reflect this and reset the increment.
        cache.start = newStart;
        cache.end = millisecondsFromEpoch;
        cache.incrementStart = WTF::msPerMonth;
        cache.incrementEnd = WTF::msPerMonth;
    } else {
        // The interval contains a DST offset change and the given time is
        // before it. Adjust the increment to avoid a linear search for
        // the offset change point and change the end of the interval.
        cache.incrementStart /= 3;
        cache.start = millisecondsFromEpoch;
    }
    // Update the offset in the cache and return it.
    cache.offset = offset;
    return offset;
}

static inline double timeToMS(double hour, double min, double sec, double ms)
{
    return (((hour * WTF::minutesPerHour + min) * WTF::secondsPerMinute + sec) * WTF::msPerSecond + ms);
}

double DateCache::gregorianDateTimeToMS(const GregorianDateTime& t, double milliseconds, WTF::TimeType inputTimeType)
{
    double day = dateToDaysFrom1970(t.year(), t.month(), t.monthDay());
    double ms = timeToMS(t.hour(), t.minute(), t.second(), milliseconds);
    double localTimeResult = (day * WTF::msPerDay) + ms;

    double localToUTCTimeOffset = inputTimeType == WTF::LocalTime ? localTimeOffset(localTimeResult, inputTimeType).offset : 0;

    return localTimeResult - localToUTCTimeOffset;
}

// input is UTC
void DateCache::msToGregorianDateTime(double millisecondsFromEpoch, WTF::TimeType outputTimeType, GregorianDateTime& tm)
{
    LocalTimeOffset localTime;
    if (outputTimeType == WTF::LocalTime) {
        localTime = localTimeOffset(millisecondsFromEpoch);
        millisecondsFromEpoch += localTime.offset;
    }
    tm = GregorianDateTime(millisecondsFromEpoch, localTime);
}

double DateCache::parseDate(JSGlobalObject* globalObject, VM& vm, const String& date)
{
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (date == m_cachedDateString)
        return m_cachedDateStringValue;
    auto expectedString = date.tryGetUtf8();
    if (!expectedString) {
        if (expectedString.error() == UTF8ConversionError::OutOfMemory)
            throwOutOfMemoryError(globalObject, scope);
        // https://tc39.github.io/ecma262/#sec-date-objects section 20.3.3.2 states that:
        // "Unrecognizable Strings or dates containing illegal element values in the
        // format String shall cause Date.parse to return NaN."
        return std::numeric_limits<double>::quiet_NaN();
    }

    auto parseDateImpl = [this] (const char* dateString) {
        bool isLocalTime;
        double value = WTF::parseES5DateFromNullTerminatedCharacters(dateString, isLocalTime);
        if (std::isnan(value))
            value = WTF::parseDateFromNullTerminatedCharacters(dateString, isLocalTime);

        if (isLocalTime)
            value -= localTimeOffset(value, WTF::LocalTime).offset;

        return value;
    };

    auto dateUtf8 = expectedString.value();
    double value = parseDateImpl(dateUtf8.data());
    m_cachedDateString = date;
    m_cachedDateStringValue = value;
    return value;
}

// https://tc39.es/ecma402/#sec-defaulttimezone
String DateCache::defaultTimeZone()
{
    icu::UnicodeString timeZoneID;
    icu::UnicodeString canonicalTimeZoneID;
    auto& timeZone = *bitwise_cast<icu::TimeZone*>(timeZoneCache());
    timeZone.getID(timeZoneID);

    UErrorCode status = U_ZERO_ERROR;
    UBool isSystem = false;
    icu::TimeZone::getCanonicalID(timeZoneID, canonicalTimeZoneID, isSystem, status);
    if (U_FAILURE(status))
        return "UTC"_s;

    String canonical = String(canonicalTimeZoneID.getBuffer(), canonicalTimeZoneID.length());
    if (isUTCEquivalent(canonical))
        return "UTC"_s;

    return canonical;
}

// To confine icu::TimeZone destructor invocation in this file.
DateCache::DateCache() = default;
DateCache::~DateCache() = default;

Ref<DateInstanceData> DateCache::cachedDateInstanceData(double millisecondsFromEpoch)
{
    return *m_dateInstanceCache.add(millisecondsFromEpoch);
}

void DateCache::timeZoneCacheSlow()
{
    // Do not use icu::TimeZone::createDefault. ICU internally has a cache for timezone and createDefault returns this cached value.
    ASSERT(!m_timeZoneCache);
    m_timeZoneCache = std::unique_ptr<OpaqueICUTimeZone, OpaqueICUTimeZoneDeleter>(bitwise_cast<OpaqueICUTimeZone*>(icu::TimeZone::detectHostTimeZone()));
}

void DateCache::reset()
{
    m_timeZoneCache.reset();
    m_utcTimeOffsetCache = LocalTimeOffsetCache();
    m_localTimeOffsetCache = LocalTimeOffsetCache();
    m_cachedDateString = String();
    m_cachedDateStringValue = std::numeric_limits<double>::quiet_NaN();
    m_dateInstanceCache.reset();
}

} // namespace JSC