Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgregoriancalendar.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6
7#include <QtCore/qdatetime.h>
8
10
11using namespace QRoundingDown;
12
13// Verification that QRoundingDown::qDivMod() works correctly:
14static_assert(qDivMod<2>(-86400).quotient == -43200);
15static_assert(qDivMod<2>(-86400).remainder == 0);
16static_assert(qDivMod<86400>(-86400).quotient == -1);
17static_assert(qDivMod<86400>(-86400).remainder == 0);
18static_assert(qDivMod<86400>(-86401).quotient == -2);
19static_assert(qDivMod<86400>(-86401).remainder == 86399);
20static_assert(qDivMod<86400>(-100000).quotient == -2);
21static_assert(qDivMod<86400>(-100000).remainder == 72800);
22static_assert(qDivMod<86400>(-172799).quotient == -2);
23static_assert(qDivMod<86400>(-172799).remainder == 1);
24static_assert(qDivMod<86400>(-172800).quotient == -2);
25static_assert(qDivMod<86400>(-172800).remainder == 0);
26
27// Uncomment to verify error on bad denominator is clear and intelligible:
28// static_assert(qDivMod<1>(17).remainder == 0);
29// static_assert(qDivMod<0>(17).remainder == 0);
30// static_assert(qDivMod<std::numeric_limits<unsigned>::max()>(17).remainder == 0);
31
32/*!
33 \since 5.14
34 \internal
35
36 \class QGregorianCalendar
37 \inmodule QtCore
38 \brief The QGregorianCalendar class implements the Gregorian calendar.
39
40 \section1 The Gregorian Calendar
41
42 The Gregorian calendar is a refinement of the earlier Julian calendar,
43 itself a late form of the Roman calendar. It is widely used.
44
45 \sa QRomanCalendar, QJulianCalendar, QCalendar
46*/
47
48QString QGregorianCalendar::name() const
49{
50 return QStringLiteral("Gregorian");
51}
52
53QStringList QGregorianCalendar::nameList()
54{
55 return {
56 QStringLiteral("Gregorian"),
57 QStringLiteral("gregory"),
58 };
59}
60
61bool QGregorianCalendar::isLeapYear(int year) const
62{
63 return leapTest(year);
64}
65
66bool QGregorianCalendar::leapTest(int year)
67{
68 if (year == QCalendar::Unspecified)
69 return false;
70
71 // No year 0 in Gregorian calendar, so -1, -5, -9 etc are leap years
72 if (year < 1)
73 ++year;
74
75 return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
76}
77
78// Duplicating code from QRomanCalendar, but inlining isLeapYear() as leapTest():
79int QGregorianCalendar::monthLength(int month, int year)
80{
81 if (month < 1 || month > 12)
82 return 0;
83
84 if (month == 2)
85 return leapTest(year) ? 29 : 28;
86
87 return 30 | ((month & 1) ^ (month >> 3));
88}
89
90bool QGregorianCalendar::validParts(int year, int month, int day)
91{
92 return year && 0 < day && day <= monthLength(month, year);
93}
94
95int QGregorianCalendar::weekDayOfJulian(qint64 jd)
96{
97 return int(qMod<7>(jd) + 1);
98}
99
100bool QGregorianCalendar::dateToJulianDay(int year, int month, int day, qint64 *jd) const
101{
102 const auto maybe = julianFromParts(year, month, day);
103 if (maybe)
104 *jd = *maybe;
105 return bool(maybe);
106}
107
108QCalendar::YearMonthDay QGregorianCalendar::julianDayToDate(qint64 jd) const
109{
110 return partsFromJulian(jd);
111}
112
113qint64
114QGregorianCalendar::matchCenturyToWeekday(const QCalendar::YearMonthDay &parts, int dow) const
115{
116 /* The Gregorian four-century cycle is a whole number of weeks long, so we
117 only need to consider four centuries, from previous through next-but-one.
118 There are thus three days of the week that can't happen, for any given
119 day-of-month, month and year-mod-100. (Exception: '00 Feb 29 has only one
120 option.)
121 */
122 auto maybe = julianFromParts(parts.year, parts.month, parts.day);
123 if (maybe) {
124 int diff = weekDayOfJulian(*maybe) - dow;
125 if (!diff)
126 return *maybe;
127 int year = parts.year < 0 ? parts.year + 1 : parts.year;
128 // What matters is the placement of leap days, so dates before March
129 // effectively belong with the dates since the preceding March:
130 const auto yearSplit = qDivMod<100>(year - (parts.month < 3 ? 1 : 0));
131 const int centuryMod4 = qMod<4>(yearSplit.quotient);
132 // Week-day shift for a century is 5, unless crossing a multiple of 400's Feb 29th.
133 static_assert(qMod<7>(36524) == 5); // and (3 * 5) % 7 = 1
134 // Formulae arrived at by case-by-case analysis of the values of
135 // centuryMod4 and diff (and the above clue to multiply by -3 = 4):
136 if (qMod<7>(diff * 4 + centuryMod4) < 4) {
137 // Century offset maps qMod<7>(diff) in {5, 6} to -1, {3, 4} to +2, and {1, 2} to +1:
138 year += (((qMod<7>(diff) + 3) / 2) % 4 - 1) * 100;
139 maybe = julianFromParts(year > 0 ? year : year - 1, parts.month, parts.day);
140 if (maybe && weekDayOfJulian(*maybe) == dow)
141 return *maybe;
142 Q_ASSERT(parts.month == 2 && parts.day == 29
143 && dow != int(Qt::Tuesday) && !(year % 100));
144 }
145
146 } else if (parts.month == 2 && parts.day == 29) {
147 int year = parts.year < 0 ? parts.year + 1 : parts.year;
148 // Feb 29th on a century needs to resolve to a multiple of 400 years.
149 const auto yearSplit = qDivMod<100>(year);
150 if (!yearSplit.remainder) {
151 const auto centuryMod4 = qMod<4>(yearSplit.quotient);
152 Q_ASSERT(centuryMod4); // or we'd have got a valid date to begin with.
153 if (centuryMod4 == 1) // round down
154 year -= 100;
155 else // 2 or 3; round up
156 year += (4 - centuryMod4) * 100;
157 maybe = julianFromParts(year > 0 ? year : year - 1, parts.month, parts.day);
158 if (maybe && weekDayOfJulian(*maybe) == dow) // (Can only happen for Tuesday.)
159 return *maybe;
160 Q_ASSERT(dow != int(Qt::Tuesday));
161 }
162 }
163 return (std::numeric_limits<qint64>::min)();
164}
165
166int QGregorianCalendar::yearStartWeekDay(int year)
167{
168 // Equivalent to weekDayOfJulian(julianForParts({year, 1, 1})
169 const int y = year - (year < 0 ? 800 : 801);
170 return qMod<7>(y + qDiv<4>(y) - qDiv<100>(y) + qDiv<400>(y)) + 1;
171}
172
173int QGregorianCalendar::yearSharingWeekDays(QDate date)
174{
175 // Returns a post-epoch year, no later than 2400, that has the same pattern
176 // of week-days (in the proleptic Gregorian calendar) as the year in which
177 // the given date falls. This will be the year in question if it's in the
178 // given range. Otherwise, the returned year's last two (decimal) digits
179 // won't coincide with the month number or day-of-month of the given date.
180 // For positive years, except when necessary to avoid such a clash, the
181 // returned year's last two digits shall coincide with those of the original
182 // year.
183
184 // Needed when formatting dates using system APIs with limited year ranges
185 // and possibly only a two-digit year. (The need to be able to safely
186 // replace the two-digit form of the returned year with a suitable form of
187 // the true year, when they don't coincide, is why the last two digits are
188 // treated specially.)
189
190 static_assert((400 * 365 + 97) % 7 == 0);
191 // A full 400-year cycle of the Gregorian calendar has 97 + 400 * 365 days;
192 // as 365 is one more than a multiple of seven and 497 is a multiple of
193 // seven, that full cycle is a whole number of weeks. So adding a multiple
194 // of four hundred years should get us a result that meets our needs.
195
196 const int year = date.year();
197 int res = (year < 1970
198 ? 2400 - (2000 - (year < 0 ? year + 1 : year)) % 400
199 : year > 2399 ? 2000 + (year - 2000) % 400 : year);
200 Q_ASSERT(res > 0);
201 if (res != year) {
202 const int lastTwo = res % 100;
203 if (lastTwo == date.month() || lastTwo == date.day()) {
204 Q_ASSERT(lastTwo && !(lastTwo & ~31));
205 // Last two digits of these years are all > 31:
206 static constexpr int usual[] = { 2198, 2199, 2098, 2099, 2399, 2298, 2299 };
207 static constexpr int leaps[] = { 2396, 2284, 2296, 2184, 2196, 2084, 2096 };
208 // Indexing is: first day of year's day-of-week, Monday = 0, one less
209 // than Qt's, as it's simpler to subtract one than to s/7/0/.
210 res = (leapTest(year) ? leaps : usual)[yearStartWeekDay(year) - 1];
211 }
212 Q_ASSERT(QDate(res, 1, 1).dayOfWeek() == QDate(year, 1, 1).dayOfWeek());
213 Q_ASSERT(QDate(res, 12, 31).dayOfWeek() == QDate(year, 12, 31).dayOfWeek());
214 }
215 Q_ASSERT(res >= 1970 && res <= 2400);
216 return res;
217}
218
219/*
220 * Math from The Calendar FAQ at http://www.tondering.dk/claus/cal/julperiod.php
221 * This formula is correct for all julian days, when using mathematical integer
222 * division (round to negative infinity), not c++11 integer division (round to zero).
223 *
224 * The source given uses 4801 BCE as base date; the following adjusts that by
225 * 4800 years to simplify part of the arithmetic (and match more closely what we
226 * do for Milankovic).
227 */
228
229using namespace QRomanCalendrical;
230// End a Gregorian four-century cycle on 1 BC's leap day:
231constexpr qint64 BaseJd = LeapDayGregorian1Bce;
232// Every four centures there are 97 leap years:
233constexpr unsigned FourCenturies = 400 * 365 + 97;
234
235std::optional<qint64> QGregorianCalendar::julianFromParts(int year, int month, int day)
236{
237 if (!validParts(year, month, day))
238 return std::nullopt;
239
240 const auto yearDays = yearMonthToYearDays(year, month);
241 const qint64 y = yearDays.year;
242 const qint64 fromYear = 365 * y + qDiv<4>(y) - qDiv<100>(y) + qDiv<400>(y);
243 return fromYear + yearDays.days + day + BaseJd;
244}
245
246QCalendar::YearMonthDay QGregorianCalendar::partsFromJulian(qint64 jd)
247{
248 const qint64 dayNumber = jd - BaseJd;
249 const qint64 century = qDiv<FourCenturies>(4 * dayNumber - 1);
250 const int dayInCentury = dayNumber - qDiv<4>(FourCenturies * century);
251
252 const int yearInCentury = qDiv<FourYears>(4 * dayInCentury - 1);
253 const int dayInYear = dayInCentury - qDiv<4>(FourYears * yearInCentury);
254 const int m = qDiv<FiveMonths>(5 * dayInYear - 3);
255 Q_ASSERT(m < 12 && m >= 0);
256 // That m is a month adjusted to March = 0, with Jan = 10, Feb = 11 in the previous year.
257 const int yearOffset = m < 10 ? 0 : 1;
258
259 const int y = 100 * century + yearInCentury + yearOffset;
260 const int month = m + 3 - 12 * yearOffset;
261 const int day = dayInYear - qDiv<5>(FiveMonths * m + 2);
262
263 // Adjust for no year 0
264 return QCalendar::YearMonthDay(y > 0 ? y : y - 1, month, day);
265}
266
267QT_END_NAMESPACE
Combined button and popup list for selecting options.
constexpr qint64 BaseJd
constexpr unsigned FourCenturies