diff options
Diffstat (limited to 'src/corelib/time/qgregoriancalendar.cpp')
-rw-r--r-- | src/corelib/time/qgregoriancalendar.cpp | 256 |
1 files changed, 171 insertions, 85 deletions
diff --git a/src/corelib/time/qgregoriancalendar.cpp b/src/corelib/time/qgregoriancalendar.cpp index ca3c19b85c..d46d24ac30 100644 --- a/src/corelib/time/qgregoriancalendar.cpp +++ b/src/corelib/time/qgregoriancalendar.cpp @@ -1,50 +1,34 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qgregoriancalendar_p.h" #include "qcalendarmath_p.h" + #include <QtCore/qdatetime.h> QT_BEGIN_NAMESPACE using namespace QRoundingDown; +// Verification that QRoundingDown::qDivMod() works correctly: +static_assert(qDivMod<2>(-86400).quotient == -43200); +static_assert(qDivMod<2>(-86400).remainder == 0); +static_assert(qDivMod<86400>(-86400).quotient == -1); +static_assert(qDivMod<86400>(-86400).remainder == 0); +static_assert(qDivMod<86400>(-86401).quotient == -2); +static_assert(qDivMod<86400>(-86401).remainder == 86399); +static_assert(qDivMod<86400>(-100000).quotient == -2); +static_assert(qDivMod<86400>(-100000).remainder == 72800); +static_assert(qDivMod<86400>(-172799).quotient == -2); +static_assert(qDivMod<86400>(-172799).remainder == 1); +static_assert(qDivMod<86400>(-172800).quotient == -2); +static_assert(qDivMod<86400>(-172800).remainder == 0); + +// Uncomment to verify error on bad denominator is clear and intelligible: +// static_assert(qDivMod<1>(17).remainder == 0); +// static_assert(qDivMod<0>(17).remainder == 0); +// static_assert(qDivMod<std::numeric_limits<unsigned>::max()>(17).remainder == 0); + /*! \since 5.14 @@ -60,18 +44,17 @@ using namespace QRoundingDown; \sa QRomanCalendar, QJulianCalendar, QCalendar */ -QGregorianCalendar::QGregorianCalendar() - : QRomanCalendar(QStringLiteral("Gregorian"), QCalendar::System::Gregorian) +QString QGregorianCalendar::name() const { - if (~calendarId()) { - Q_ASSERT(calendarSystem() == QCalendar::System::Gregorian); - registerAlias(QStringLiteral("gregory")); - } // else: being ignored in favor of a duplicate created at the same time + return QStringLiteral("Gregorian"); } -QString QGregorianCalendar::name() const +QStringList QGregorianCalendar::nameList() { - return QStringLiteral("Gregorian"); + return { + QStringLiteral("Gregorian"), + QStringLiteral("gregory"), + }; } bool QGregorianCalendar::isLeapYear(int year) const @@ -110,71 +93,174 @@ bool QGregorianCalendar::validParts(int year, int month, int day) int QGregorianCalendar::weekDayOfJulian(qint64 jd) { - return qMod(jd, 7) + 1; + return int(qMod<7>(jd) + 1); } bool QGregorianCalendar::dateToJulianDay(int year, int month, int day, qint64 *jd) const { - return julianFromParts(year, month, day, jd); + const auto maybe = julianFromParts(year, month, day); + if (maybe) + *jd = *maybe; + return bool(maybe); } -bool QGregorianCalendar::julianFromParts(int year, int month, int day, qint64 *jd) +QCalendar::YearMonthDay QGregorianCalendar::julianDayToDate(qint64 jd) const { - Q_ASSERT(jd); - if (!validParts(year, month, day)) - return false; + return partsFromJulian(jd); +} - if (year < 0) - ++year; +qint64 +QGregorianCalendar::matchCenturyToWeekday(const QCalendar::YearMonthDay &parts, int dow) const +{ + /* The Gregorian four-century cycle is a whole number of weeks long, so we + only need to consider four centuries, from previous through next-but-one. + There are thus three days of the week that can't happen, for any given + day-of-month, month and year-mod-100. (Exception: '00 Feb 29 has only one + option.) + */ + auto maybe = julianFromParts(parts.year, parts.month, parts.day); + if (maybe) { + int diff = weekDayOfJulian(*maybe) - dow; + if (!diff) + return *maybe; + int year = parts.year < 0 ? parts.year + 1 : parts.year; + // What matters is the placement of leap days, so dates before March + // effectively belong with the dates since the preceding March: + const auto yearSplit = qDivMod<100>(year - (parts.month < 3 ? 1 : 0)); + const int centuryMod4 = qMod<4>(yearSplit.quotient); + // Week-day shift for a century is 5, unless crossing a multiple of 400's Feb 29th. + static_assert(qMod<7>(36524) == 5); // and (3 * 5) % 7 = 1 + // Formulae arrived at by case-by-case analysis of the values of + // centuryMod4 and diff (and the above clue to multiply by -3 = 4): + if (qMod<7>(diff * 4 + centuryMod4) < 4) { + // Century offset maps qMod<7>(diff) in {5, 6} to -1, {3, 4} to +2, and {1, 2} to +1: + year += (((qMod<7>(diff) + 3) / 2) % 4 - 1) * 100; + maybe = julianFromParts(year > 0 ? year : year - 1, parts.month, parts.day); + if (maybe && weekDayOfJulian(*maybe) == dow) + return *maybe; + Q_ASSERT(parts.month == 2 && parts.day == 29 + && dow != int(Qt::Tuesday) && !(year % 100)); + } - /* - * Math from The Calendar FAQ at http://www.tondering.dk/claus/cal/julperiod.php - * This formula is correct for all julian days, when using mathematical integer - * division (round to negative infinity), not c++11 integer division (round to zero) - */ - int a = month < 3 ? 1 : 0; - qint64 y = qint64(year) + 4800 - a; - int m = month + 12 * a - 3; - *jd = day + qDiv(153 * m + 2, 5) - 32045 - + 365 * y + qDiv(y, 4) - qDiv(y, 100) + qDiv(y, 400); - return true; + } else if (parts.month == 2 && parts.day == 29) { + int year = parts.year < 0 ? parts.year + 1 : parts.year; + // Feb 29th on a century needs to resolve to a multiple of 400 years. + const auto yearSplit = qDivMod<100>(year); + if (!yearSplit.remainder) { + const auto centuryMod4 = qMod<4>(yearSplit.quotient); + Q_ASSERT(centuryMod4); // or we'd have got a valid date to begin with. + if (centuryMod4 == 1) // round down + year -= 100; + else // 2 or 3; round up + year += (4 - centuryMod4) * 100; + maybe = julianFromParts(year > 0 ? year : year - 1, parts.month, parts.day); + if (maybe && weekDayOfJulian(*maybe) == dow) // (Can only happen for Tuesday.) + return *maybe; + Q_ASSERT(dow != int(Qt::Tuesday)); + } + } + return (std::numeric_limits<qint64>::min)(); } int QGregorianCalendar::yearStartWeekDay(int year) { // Equivalent to weekDayOfJulian(julianForParts({year, 1, 1}) const int y = year - (year < 0 ? 800 : 801); - return (y + qDiv(y, 4) - qDiv(y, 100) + qDiv(y, 400)) % 7 + 1; + return qMod<7>(y + qDiv<4>(y) - qDiv<100>(y) + qDiv<400>(y)) + 1; } -QCalendar::YearMonthDay QGregorianCalendar::julianDayToDate(qint64 jd) const +int QGregorianCalendar::yearSharingWeekDays(QDate date) { - return partsFromJulian(jd); + // Returns a post-epoch year, no later than 2400, that has the same pattern + // of week-days (in the proleptic Gregorian calendar) as the year in which + // the given date falls. This will be the year in question if it's in the + // given range. Otherwise, the returned year's last two (decimal) digits + // won't coincide with the month number or day-of-month of the given date. + // For positive years, except when necessary to avoid such a clash, the + // returned year's last two digits shall coincide with those of the original + // year. + + // Needed when formatting dates using system APIs with limited year ranges + // and possibly only a two-digit year. (The need to be able to safely + // replace the two-digit form of the returned year with a suitable form of + // the true year, when they don't coincide, is why the last two digits are + // treated specially.) + + static_assert((400 * 365 + 97) % 7 == 0); + // A full 400-year cycle of the Gregorian calendar has 97 + 400 * 365 days; + // as 365 is one more than a multiple of seven and 497 is a multiple of + // seven, that full cycle is a whole number of weeks. So adding a multiple + // of four hundred years should get us a result that meets our needs. + + const int year = date.year(); + int res = (year < 1970 + ? 2400 - (2000 - (year < 0 ? year + 1 : year)) % 400 + : year > 2399 ? 2000 + (year - 2000) % 400 : year); + Q_ASSERT(res > 0); + if (res != year) { + const int lastTwo = res % 100; + if (lastTwo == date.month() || lastTwo == date.day()) { + Q_ASSERT(lastTwo && !(lastTwo & ~31)); + // Last two digits of these years are all > 31: + static constexpr int usual[] = { 2198, 2199, 2098, 2099, 2399, 2298, 2299 }; + static constexpr int leaps[] = { 2396, 2284, 2296, 2184, 2196, 2084, 2096 }; + // Indexing is: first day of year's day-of-week, Monday = 0, one less + // than Qt's, as it's simpler to subtract one than to s/7/0/. + res = (leapTest(year) ? leaps : usual)[yearStartWeekDay(year) - 1]; + } + Q_ASSERT(QDate(res, 1, 1).dayOfWeek() == QDate(year, 1, 1).dayOfWeek()); + Q_ASSERT(QDate(res, 12, 31).dayOfWeek() == QDate(year, 12, 31).dayOfWeek()); + } + Q_ASSERT(res >= 1970 && res <= 2400); + return res; +} + +/* + * Math from The Calendar FAQ at http://www.tondering.dk/claus/cal/julperiod.php + * This formula is correct for all julian days, when using mathematical integer + * division (round to negative infinity), not c++11 integer division (round to zero). + * + * The source given uses 4801 BCE as base date; the following adjusts that by + * 4800 years to simplify part of the arithmetic (and match more closely what we + * do for Milankovic). + */ + +using namespace QRomanCalendrical; +// End a Gregorian four-century cycle on 1 BC's leap day: +constexpr qint64 BaseJd = LeapDayGregorian1Bce; +// Every four centures there are 97 leap years: +constexpr unsigned FourCenturies = 400 * 365 + 97; + +std::optional<qint64> QGregorianCalendar::julianFromParts(int year, int month, int day) +{ + if (!validParts(year, month, day)) + return std::nullopt; + + const auto yearDays = yearMonthToYearDays(year, month); + const qint64 y = yearDays.year; + const qint64 fromYear = 365 * y + qDiv<4>(y) - qDiv<100>(y) + qDiv<400>(y); + return fromYear + yearDays.days + day + BaseJd; } QCalendar::YearMonthDay QGregorianCalendar::partsFromJulian(qint64 jd) { - /* - * Math from The Calendar FAQ at http://www.tondering.dk/claus/cal/julperiod.php - * This formula is correct for all julian days, when using mathematical integer - * division (round to negative infinity), not c++11 integer division (round to zero) - */ - qint64 a = jd + 32044; - qint64 b = qDiv(4 * a + 3, 146097); - int c = a - qDiv(146097 * b, 4); + const qint64 dayNumber = jd - BaseJd; + const qint64 century = qDiv<FourCenturies>(4 * dayNumber - 1); + const int dayInCentury = dayNumber - qDiv<4>(FourCenturies * century); - int d = qDiv(4 * c + 3, 1461); - int e = c - qDiv(1461 * d, 4); - int m = qDiv(5 * e + 2, 153); + const int yearInCentury = qDiv<FourYears>(4 * dayInCentury - 1); + const int dayInYear = dayInCentury - qDiv<4>(FourYears * yearInCentury); + const int m = qDiv<FiveMonths>(5 * dayInYear - 3); + Q_ASSERT(m < 12 && m >= 0); + // That m is a month adjusted to March = 0, with Jan = 10, Feb = 11 in the previous year. + const int yearOffset = m < 10 ? 0 : 1; - int y = 100 * b + d - 4800 + qDiv(m, 10); + const int y = 100 * century + yearInCentury + yearOffset; + const int month = m + 3 - 12 * yearOffset; + const int day = dayInYear - qDiv<5>(FiveMonths * m + 2); // Adjust for no year 0 - int year = y > 0 ? y : y - 1; - int month = m + 3 - 12 * qDiv(m, 10); - int day = e - qDiv(153 * m + 2, 5) + 1; - - return QCalendar::YearMonthDay(year, month, day); + return QCalendar::YearMonthDay(y > 0 ? y : y - 1, month, day); } QT_END_NAMESPACE |