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
qdatetimeparser.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 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// Qt-Security score:critical reason:data-parser
4
5#include "private/qdatetimeparser_p.h"
6
7#include "qdatastream.h"
8#include "private/qdatetime_p.h"
9#include "qdebug.h"
10#include "qlocale.h"
11#include "private/qlocale_p.h"
12#include "private/qlocale_tools_p.h"
13#include "qset.h"
14#include "private/qstringiterator_p.h"
15#include "private/qtenvironmentvariables_p.h"
16#include "qtimezone.h"
17#if QT_CONFIG(timezone)
18#include "private/qtimezoneprivate_p.h"
19#endif
21
22
23//#define QDATETIMEPARSER_DEBUG
24#if defined (QDATETIMEPARSER_DEBUG) && !defined(QT_NO_DEBUG_STREAM)
25# define QDTPDEBUG qDebug()
26# define QDTPDEBUGN qDebug
27#else
28# define QDTPDEBUG if (false) qDebug()
29# define QDTPDEBUGN if (false) qDebug
30#endif
31
33
34constexpr int QDateTimeParser::NoSectionIndex;
35constexpr int QDateTimeParser::FirstSectionIndex;
36constexpr int QDateTimeParser::LastSectionIndex;
37
38using namespace Qt::StringLiterals;
39
40template <typename T>
41using ShortVector = QVarLengthArray<T, 13>; // enough for month (incl. leap) and day-of-week names
42
43QDateTimeParser::~QDateTimeParser()
44{
45}
46
47/*!
48 \internal
49 Gets the digit from a datetime. E.g.
50
51 QDateTime var(QDate(2004, 02, 02));
52 int digit = getDigit(var, Year);
53 // digit = 2004
54*/
55
56int QDateTimeParser::getDigit(const QDateTime &t, int index) const
57{
58 if (index < 0 || index >= sectionNodes.size()) {
59 qWarning("QDateTimeParser::getDigit() Internal error (%ls %d)",
60 qUtf16Printable(t.toString()), index);
61 return -1;
62 }
63 const SectionNode &node = sectionNodes.at(index);
64 switch (node.type) {
65 case TimeZoneSection: return t.offsetFromUtc();
66 case Hour24Section: case Hour12Section: return t.time().hour();
67 case MinuteSection: return t.time().minute();
68 case SecondSection: return t.time().second();
69 case MSecSection: return t.time().msec();
70 case YearSection2Digits:
71 case YearSection: return t.date().year(calendar);
72 case MonthSection: return t.date().month(calendar);
73 case DaySection: return t.date().day(calendar);
74 case DayOfWeekSectionShort:
75 case DayOfWeekSectionLong: return calendar.dayOfWeek(t.date());
76 case AmPmSection: return t.time().hour() > 11 ? 1 : 0;
77
78 default: break;
79 }
80
81 qWarning("QDateTimeParser::getDigit() Internal error 2 (%ls %d)",
82 qUtf16Printable(t.toString()), index);
83 return -1;
84}
85
86/*!
87 \internal
88 Difference between two days of the week.
89
90 Returns a difference in the range from -3 through +3, so that steps by small
91 numbers of days move us through the month in the same direction as through
92 the week.
93*/
94
95static int dayOfWeekDiff(int sought, int held)
96{
97 const int diff = sought - held;
98 return diff < -3 ? diff + 7 : diff > 3 ? diff - 7 : diff;
99}
100
101static bool preferDayOfWeek(const QList<QDateTimeParser::SectionNode> &nodes)
102{
103 // True precisely if there is a day-of-week field but no day-of-month field.
104 bool result = false;
105 for (const auto &node : nodes) {
106 if (node.type & QDateTimeParser::DaySection)
107 return false;
108 if (node.type & QDateTimeParser::DayOfWeekSectionMask)
109 result = true;
110 }
111 return result;
112}
113
114/*!
115 \internal
116 Sets a digit in a datetime. E.g.
117
118 QDateTime var(QDate(2004, 02, 02));
119 int digit = getDigit(var, Year);
120 // digit = 2004
121 setDigit(&var, Year, 2005);
122 digit = getDigit(var, Year);
123 // digit = 2005
124*/
125
126bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
127{
128 if (index < 0 || index >= sectionNodes.size()) {
129 qWarning("QDateTimeParser::setDigit() Internal error (%ls %d %d)",
130 qUtf16Printable(v.toString()), index, newVal);
131 return false;
132 }
133
134 const QDate oldDate = v.date();
135 QCalendar::YearMonthDay date = calendar.partsFromDate(oldDate);
136 if (!date.isValid())
137 return false;
138 int weekDay = calendar.dayOfWeek(oldDate);
139 enum { NoFix, MonthDay, WeekDay } fixDay = NoFix;
140
141 const QTime time = v.time();
142 int hour = time.hour();
143 int minute = time.minute();
144 int second = time.second();
145 int msec = time.msec();
146 QTimeZone timeZone = v.timeRepresentation();
147
148 const SectionNode &node = sectionNodes.at(index);
149 switch (node.type) {
150 case Hour24Section: case Hour12Section: hour = newVal; break;
151 case MinuteSection: minute = newVal; break;
152 case SecondSection: second = newVal; break;
153 case MSecSection: msec = newVal; break;
154 case YearSection2Digits:
155 case YearSection: date.year = newVal; break;
156 case MonthSection: date.month = newVal; break;
157 case DaySection:
158 if (newVal > 31) {
159 // have to keep legacy behavior. setting the
160 // date to 32 should return false. Setting it
161 // to 31 for february should return true
162 return false;
163 }
164 date.day = newVal;
165 fixDay = MonthDay;
166 break;
167 case DayOfWeekSectionShort:
168 case DayOfWeekSectionLong:
169 if (newVal > 7 || newVal <= 0)
170 return false;
171 date.day += dayOfWeekDiff(newVal, weekDay);
172 weekDay = newVal;
173 fixDay = WeekDay;
174 break;
175 case TimeZoneSection:
176 if (newVal < absoluteMin(index) || newVal > absoluteMax(index))
177 return false;
178 // Only offset from UTC is amenable to setting an int value:
179 timeZone = QTimeZone::fromSecondsAheadOfUtc(newVal);
180 break;
181 case AmPmSection: hour = (newVal == 0 ? hour % 12 : (hour % 12) + 12); break;
182 default:
183 qWarning("QDateTimeParser::setDigit() Internal error (%ls)",
184 qUtf16Printable(node.name()));
185 break;
186 }
187
188 if (!(node.type & DaySectionMask)) {
189 if (date.day < cachedDay)
190 date.day = cachedDay;
191 fixDay = MonthDay;
192 if (weekDay > 0 && weekDay <= 7 && preferDayOfWeek(sectionNodes)) {
193 const int max = calendar.daysInMonth(date.month, date.year);
194 if (max > 0 && date.day > max)
195 date.day = max;
196 const int newDoW = calendar.dayOfWeek(calendar.dateFromParts(date));
197 if (newDoW > 0 && newDoW <= 7)
198 date.day += dayOfWeekDiff(weekDay, newDoW);
199 fixDay = WeekDay;
200 }
201 }
202
203 if (fixDay != NoFix) {
204 const int max = calendar.daysInMonth(date.month, date.year);
205 // max > 0 precisely if the year does have such a month
206 if (max > 0 && date.day > max)
207 date.day = fixDay == WeekDay ? date.day - 7 : max;
208 else if (date.day < 1)
209 date.day = fixDay == WeekDay ? date.day + 7 : 1;
210 Q_ASSERT(fixDay != WeekDay
211 || calendar.dayOfWeek(calendar.dateFromParts(date)) == weekDay);
212 }
213
214 const QDate newDate = calendar.dateFromParts(date);
215 const QTime newTime(hour, minute, second, msec);
216 if (!newDate.isValid() || !newTime.isValid())
217 return false;
218
219 v = QDateTime(newDate, newTime, timeZone);
220 return true;
221}
222
223
224
225/*!
226 \internal
227
228 Returns the absolute maximum for a section
229*/
230
231int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const
232{
233 const SectionNode &sn = sectionNode(s);
234 switch (sn.type) {
235 case TimeZoneSection:
236 return QTimeZone::MaxUtcOffsetSecs;
237 case Hour24Section:
238 case Hour12Section:
239 // This is special-cased in parseSection.
240 // We want it to be 23 for the stepBy case.
241 return 23;
242 case MinuteSection:
243 case SecondSection:
244 return 59;
245 case MSecSection:
246 return 999;
247 case YearSection2Digits:
248 // sectionMaxSize will prevent people from typing in a larger number in
249 // count == 2 sections; stepBy() will work on real years anyway.
250 case YearSection:
251 return 9999;
252 case MonthSection:
253 return calendar.maximumMonthsInYear();
254 case DaySection:
255 return cur.isValid() ? cur.date().daysInMonth(calendar) : calendar.maximumDaysInMonth();
256 case DayOfWeekSectionShort:
257 case DayOfWeekSectionLong:
258 return 7;
259 case AmPmSection:
260 return int(UpperCase);
261 default:
262 break;
263 }
264 qWarning("QDateTimeParser::absoluteMax() Internal error (%ls)",
265 qUtf16Printable(sn.name()));
266 return -1;
267}
268
269/*!
270 \internal
271
272 Returns the absolute minimum for a section
273*/
274
275int QDateTimeParser::absoluteMin(int s) const
276{
277 const SectionNode &sn = sectionNode(s);
278 switch (sn.type) {
279 case TimeZoneSection:
280 return QTimeZone::MinUtcOffsetSecs;
281 case Hour24Section:
282 case Hour12Section:
283 case MinuteSection:
284 case SecondSection:
285 case MSecSection:
286 case YearSection2Digits:
287 return 0;
288 case YearSection:
289 return -9999;
290 case MonthSection:
291 case DaySection:
292 case DayOfWeekSectionShort:
293 case DayOfWeekSectionLong: return 1;
294 case AmPmSection: return int(NativeCase);
295 default: break;
296 }
297 qWarning("QDateTimeParser::absoluteMin() Internal error (%ls, %0x)",
298 qUtf16Printable(sn.name()), sn.type);
299 return -1;
300}
301
302/*!
303 \internal
304
305 Returns the sectionNode for the Section \a s.
306*/
307
308const QDateTimeParser::SectionNode &QDateTimeParser::sectionNode(int sectionIndex) const
309{
310 static constexpr SectionNode first{FirstSection, 0, -1, 0};
311 static constexpr SectionNode last{LastSection, -1, -1, 0};
312 static constexpr SectionNode none{NoSection, -1, -1, 0};
313 if (sectionIndex < 0) {
314 switch (sectionIndex) {
315 case FirstSectionIndex:
316 return first;
317 case LastSectionIndex:
318 return last;
319 case NoSectionIndex:
320 return none;
321 }
322 } else if (sectionIndex < sectionNodes.size()) {
323 return sectionNodes.at(sectionIndex);
324 }
325
326 qWarning("QDateTimeParser::sectionNode() Internal error (%d)",
327 sectionIndex);
328 return none;
329}
330
331QDateTimeParser::Section QDateTimeParser::sectionType(int sectionIndex) const
332{
333 return sectionNode(sectionIndex).type;
334}
335
336
337/*!
338 \internal
339
340 Returns the starting position for section \a s.
341*/
342
343int QDateTimeParser::sectionPos(int sectionIndex) const
344{
345 return sectionPos(sectionNode(sectionIndex));
346}
347
348int QDateTimeParser::sectionPos(SectionNode sn) const
349{
350 switch (sn.type) {
351 case FirstSection: return 0;
352 case LastSection: return displayText().size() - 1;
353 default: break;
354 }
355 if (sn.pos == -1) {
356 qWarning("QDateTimeParser::sectionPos Internal error (%ls)", qUtf16Printable(sn.name()));
357 return -1;
358 }
359 return sn.pos;
360}
361
362/*!
363 \internal
364
365 Helper function for parseSection.
366*/
367
368static qsizetype digitCount(QStringView str)
369{
370 qsizetype digits = 0;
371 for (QStringIterator it(str); it.hasNext();) {
372 if (!QChar::isDigit(it.next()))
373 break;
374 digits++;
375 }
376 return digits;
377}
378
379/*!
380 \internal
381
382 helper function for parseFormat. removes quotes that are
383 not escaped and removes the escaping on those that are escaped
384
385*/
386static QString unquote(QStringView str)
387{
388 // ### Align unquoting format strings for both from/toString(), QTBUG-110669
389 const QLatin1Char quote('\'');
390 const QLatin1Char slash('\\');
391 const QLatin1Char zero('0');
392 QString ret;
393 QChar status(zero);
394 const int max = str.size();
395 for (int i=0; i<max; ++i) {
396 if (str.at(i) == quote) {
397 if (status != quote)
398 status = quote;
399 else if (!ret.isEmpty() && str.at(i - 1) == slash)
400 ret[ret.size() - 1] = quote;
401 else
402 status = zero;
403 } else {
404 ret += str.at(i);
405 }
406 }
407 return ret;
408}
409
410static inline int countRepeat(QStringView str, int index, int maxCount)
411{
412 str = str.sliced(index);
413 if (maxCount < str.size())
414 str = str.first(maxCount);
415
416 return qt_repeatCount(str);
417}
418
419static inline void appendSeparator(QStringList *list, QStringView string,
420 int from, int size, int lastQuote)
421{
422 Q_ASSERT(size >= 0 && from + size <= string.size());
423 const QStringView separator = string.sliced(from, size);
424 list->append(lastQuote >= from ? unquote(separator) : separator.toString());
425}
426
427/*!
428 \internal
429
430 Parses the format \a newFormat. If successful, returns \c true and sets up
431 the format. Else keeps the old format and returns \c false.
432*/
433bool QDateTimeParser::parseFormat(QStringView newFormat)
434{
435 const QLatin1Char quote('\'');
436 const QLatin1Char slash('\\');
437 const QLatin1Char zero('0');
438 if (newFormat == displayFormat && !newFormat.isEmpty())
439 return true;
440
441 QDTPDEBUGN("parseFormat: %s", newFormat.toLatin1().constData());
442
443 QList<SectionNode> newSectionNodes;
444 Sections newDisplay;
445 QStringList newSeparators;
446 int i, index = 0;
447 int add = 0;
448 QLatin1Char status(zero);
449 const int max = newFormat.size();
450 int lastQuote = -1;
451 for (i = 0; i<max; ++i) {
452 if (newFormat.at(i) == quote) {
453 lastQuote = i;
454 ++add;
455 if (status != quote)
456 status = quote;
457 else if (i > 0 && newFormat.at(i - 1) != slash)
458 status = zero;
459 } else if (status != quote) {
460 const char sect = newFormat.at(i).toLatin1();
461 switch (sect) {
462 case 'H':
463 case 'h':
464 if (parserType != QMetaType::QDate) {
465 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
466 const Section hour = (sect == 'h') ? Hour12Section : Hour24Section;
467 const SectionNode sn{hour, i - add, countRepeat(newFormat, i, 2)};
468 newSectionNodes.append(sn);
469 i += sn.count - 1;
470 index = i + 1;
471 newDisplay |= hour;
472 }
473 break;
474 case 'm':
475 if (parserType != QMetaType::QDate) {
476 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
477 const SectionNode sn{MinuteSection, i - add, countRepeat(newFormat, i, 2)};
478 newSectionNodes.append(sn);
479 i += sn.count - 1;
480 index = i + 1;
481 newDisplay |= MinuteSection;
482 }
483 break;
484 case 's':
485 if (parserType != QMetaType::QDate) {
486 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
487 const SectionNode sn{SecondSection, i - add, countRepeat(newFormat, i, 2)};
488 newSectionNodes.append(sn);
489 i += sn.count - 1;
490 index = i + 1;
491 newDisplay |= SecondSection;
492 }
493 break;
494
495 case 'z':
496 if (parserType != QMetaType::QDate) {
497 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
498 const int repeat = countRepeat(newFormat, i, 3);
499 const SectionNode sn{MSecSection, i - add, repeat < 3 ? 1 : 3};
500 newSectionNodes.append(sn);
501 i += repeat - 1;
502 index = i + 1;
503 newDisplay |= MSecSection;
504 }
505 break;
506 case 'A':
507 case 'a':
508 if (parserType != QMetaType::QDate) {
509 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
510 const int pos = i - add;
511 Case caseOpt = sect == 'A' ? UpperCase : LowerCase;
512 newDisplay |= AmPmSection;
513 if (i + 1 < newFormat.size()
514 && newFormat.sliced(i + 1).startsWith(u'p', Qt::CaseInsensitive)) {
515 ++i;
516 if (newFormat.at(i) != QLatin1Char(caseOpt == UpperCase ? 'P' : 'p'))
517 caseOpt = NativeCase;
518 }
519 const SectionNode sn{AmPmSection, pos, int(caseOpt)};
520 newSectionNodes.append(sn);
521 index = i + 1;
522 }
523 break;
524 case 'y':
525 if (parserType != QMetaType::QTime) {
526 const int repeat = countRepeat(newFormat, i, 4);
527 if (repeat >= 2) {
528 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
529 const SectionNode sn{repeat == 4 ? YearSection : YearSection2Digits,
530 i - add, repeat == 4 ? 4 : 2};
531 newSectionNodes.append(sn);
532 i += sn.count - 1;
533 index = i + 1;
534 newDisplay |= sn.type;
535 }
536 }
537 break;
538 case 'M':
539 if (parserType != QMetaType::QTime) {
540 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
541 const SectionNode sn{MonthSection, i - add, countRepeat(newFormat, i, 4)};
542 newSectionNodes.append(sn);
543 i += sn.count - 1;
544 index = i + 1;
545 newDisplay |= MonthSection;
546 }
547 break;
548 case 'd':
549 if (parserType != QMetaType::QTime) {
550 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
551 const int repeat = countRepeat(newFormat, i, 4);
552 const Section sectionType = (repeat == 4 ? DayOfWeekSectionLong
553 : (repeat == 3 ? DayOfWeekSectionShort : DaySection));
554 const SectionNode sn{sectionType, i - add, repeat};
555 newSectionNodes.append(sn);
556 i += sn.count - 1;
557 index = i + 1;
558 newDisplay |= sn.type;
559 }
560 break;
561 case 't':
562 if (parserType == QMetaType::QDateTime) {
563 appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
564 const SectionNode sn{TimeZoneSection, i - add, countRepeat(newFormat, i, 4)};
565 newSectionNodes.append(sn);
566 i += sn.count - 1;
567 index = i + 1;
568 newDisplay |= TimeZoneSection;
569 }
570 break;
571 default:
572 break;
573 }
574 }
575 }
576 if (newSectionNodes.isEmpty() && context == DateTimeEdit)
577 return false;
578
579 if ((newDisplay & (AmPmSection|Hour12Section)) == Hour12Section) {
580 const int count = newSectionNodes.size();
581 for (int i = 0; i < count; ++i) {
582 SectionNode &node = newSectionNodes[i];
583 if (node.type == Hour12Section)
584 node.type = Hour24Section;
585 }
586 }
587
588 if (index < max)
589 appendSeparator(&newSeparators, newFormat, index, max - index, lastQuote);
590 else
591 newSeparators.append(QString());
592
593 displayFormat = newFormat.toString();
594 separators = newSeparators;
595 sectionNodes = newSectionNodes;
596 display = newDisplay;
597
598// for (int i=0; i<sectionNodes.size(); ++i) {
599// QDTPDEBUG << sectionNodes.at(i).name() << sectionNodes.at(i).count;
600// }
601
602 QDTPDEBUG << newFormat << displayFormat;
603 QDTPDEBUGN("separators:\n'%s'", separators.join("\n"_L1).toLatin1().constData());
604
605 return true;
606}
607
608/*!
609 \internal
610
611 Returns the size of section \a s.
612*/
613
614int QDateTimeParser::sectionSize(int sectionIndex) const
615{
616 if (sectionIndex < 0)
617 return 0;
618
619 if (sectionIndex >= sectionNodes.size()) {
620 qWarning("QDateTimeParser::sectionSize Internal error (%d)", sectionIndex);
621 return -1;
622 }
623
624 if (sectionIndex == sectionNodes.size() - 1) {
625 // In some cases there is a difference between displayText() and text.
626 // e.g. when text is 2000/01/31 and displayText() is "2000/2/31" - text
627 // is the previous value and displayText() is the new value.
628 // The size difference is always due to leading zeroes.
629 int sizeAdjustment = 0;
630 const int displayTextSize = displayText().size();
631 if (displayTextSize != m_text.size()) {
632 // Any zeroes added before this section will affect our size.
633 int preceedingZeroesAdded = 0;
634 if (sectionNodes.size() > 1 && context == DateTimeEdit) {
635 const auto begin = sectionNodes.cbegin();
636 const auto end = begin + sectionIndex;
637 for (auto sectionIt = begin; sectionIt != end; ++sectionIt)
638 preceedingZeroesAdded += sectionIt->zeroesAdded;
639 }
640 sizeAdjustment = preceedingZeroesAdded;
641 }
642
643 return displayTextSize + sizeAdjustment - sectionPos(sectionIndex) - separators.last().size();
644 } else {
645 return sectionPos(sectionIndex + 1) - sectionPos(sectionIndex)
646 - separators.at(sectionIndex + 1).size();
647 }
648}
649
650
651int QDateTimeParser::sectionMaxSize(Section s, int count) const
652{
653#if QT_CONFIG(textdate)
654 int mcount = calendar.maximumMonthsInYear();
655#endif
656
657 switch (s) {
658 case FirstSection:
659 case NoSection:
660 case LastSection:
661 return 0;
662
663 case AmPmSection:
664 // Special: "count" here is a case flag, not field width !
665 return qMax(getAmPmText(AmText, Case(count)).size(),
666 getAmPmText(PmText, Case(count)).size());
667
668 case Hour24Section:
669 case Hour12Section:
670 case MinuteSection:
671 case SecondSection:
672 case DaySection:
673 return 2;
674
675 case DayOfWeekSectionShort:
676 case DayOfWeekSectionLong:
677#if QT_CONFIG(textdate)
678 mcount = 7;
679 Q_FALLTHROUGH();
680#endif
681 case MonthSection:
682#if QT_CONFIG(textdate)
683 if (count <= 2)
684 return 2;
685
686 {
687 int ret = 0;
688 const QLocale l = locale();
689 const QLocale::FormatType format = count == 4 ? QLocale::LongFormat : QLocale::ShortFormat;
690 for (int i=1; i<=mcount; ++i) {
691 const QString str = (s == MonthSection
692 ? calendar.monthName(l, i, QCalendar::Unspecified, format)
693 : l.dayName(i, format));
694 ret = qMax(str.size(), ret);
695 }
696 return ret;
697 }
698#else
699 return 2;
700#endif // textdate
701 case MSecSection:
702 return 3;
703 case YearSection:
704 return 4;
705 case YearSection2Digits:
706 return 2;
707 case TimeZoneSection:
708 switch (count) { // See findTimeZone()'s documentation.
709 case 1: // any recognized format
710 break;
711 case 2: // simple offset format, no colons [sign]HHmm
712 return 5;
713 case 3: // simple offset format, with colons [sign]HH:mm
714 return 6;
715 case 4: // only a zone name
716 break;
717 }
718 // IANA ID is arbitrarily many tokens (each up to 14 bytes) joined with
719 // '/' separators. Other forms of name are unconstrained.
720 return std::numeric_limits<int>::max();
721
722 case CalendarPopupSection:
723 case Internal:
724 case TimeSectionMask:
725 case DateSectionMask:
726 case HourSectionMask:
727 case YearSectionMask:
728 case DayOfWeekSectionMask:
729 case DaySectionMask:
730 qWarning("QDateTimeParser::sectionMaxSize: Invalid section %s",
731 SectionNode::name(s).toLatin1().constData());
732 break;
733 }
734 return -1;
735}
736
737
738int QDateTimeParser::sectionMaxSize(int index) const
739{
740 const SectionNode &sn = sectionNode(index);
741 return sectionMaxSize(sn.type, sn.count);
742}
743
744// Separator matching
745//
746// QTBUG-114909: user may be oblivious to difference between visibly
747// indistinguishable spacing characters. For now we only treat horizontal
748// spacing characters, excluding tab, as equivalent.
749
750static int matchesSeparator(QStringView text, QStringView separator)
751{
752 const auto isSimpleSpace = [](char32_t ch) {
753 // Distinguish tab, CR and the vertical spaces from the rest:
754 return ch == u' ' || (ch > 127 && QChar::isSpace(ch));
755 };
756 // -1 if not a match, else length of prefix of text that does match.
757 // First check for exact match
758 if (!text.startsWith(separator)) {
759 // Failing that, check for space-identifying match:
760 QStringIterator given(text), sep(separator);
761 while (sep.hasNext()) {
762 if (!given.hasNext())
763 return -1;
764 char32_t s = sep.next(), g = given.next();
765 if (s != g && !(isSimpleSpace(s) && isSimpleSpace(g)))
766 return -1;
767 }
768 // One side may have used a surrogate pair space where the other didn't:
769 return given.index();
770 }
771 return separator.size();
772}
773
774/*!
775 \internal
776
777 Returns the text of section \a s. This function operates on the
778 arg text rather than edit->text().
779*/
780
781
782QString QDateTimeParser::sectionText(const QString &text, int sectionIndex, int index) const
783{
784 return text.mid(index, sectionSize(sectionIndex));
785}
786
787QString QDateTimeParser::sectionText(int sectionIndex) const
788{
789 const SectionNode &sn = sectionNode(sectionIndex);
790 return sectionText(displayText(), sectionIndex, sn.pos);
791}
792
793QDateTimeParser::ParsedSection
794QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, int offset) const
795{
796 ParsedSection result; // initially Invalid
797 const SectionNode &sn = sectionNode(sectionIndex);
798 Q_ASSERT_X(!(sn.type & Internal),
799 "QDateTimeParser::parseSection", "Internal error");
800
801 const int sectionmaxsize = sectionMaxSize(sectionIndex);
802 const bool negate = (sn.type == YearSection && m_text.size() > offset
803 && calendar.isProleptic() && m_text.at(offset) == u'-');
804 const int negativeYearOffset = negate ? 1 : 0;
805
806 QStringView sectionTextRef =
807 QStringView { m_text }.mid(offset + negativeYearOffset, sectionmaxsize);
808
809 QDTPDEBUG << "sectionValue for" << sn.name()
810 << "with text" << m_text << "and (at" << offset
811 << ") st:" << sectionTextRef;
812
813 switch (sn.type) {
814 case AmPmSection: {
815 QString sectiontext = sectionTextRef.toString();
816 int used;
817 const int ampm = findAmPm(sectiontext, sectionIndex, &used);
818 switch (ampm) {
819 case AM: // sectiontext == AM
820 case PM: // sectiontext == PM
821 result = ParsedSection(Acceptable, ampm, used);
822 break;
823 case PossibleAM: // sectiontext => AM
824 case PossiblePM: // sectiontext => PM
825 result = ParsedSection(Intermediate, ampm - 2, used);
826 break;
827 case PossibleBoth: // sectiontext => AM|PM
828 result = ParsedSection(Intermediate, 0, used);
829 break;
830 case Neither:
831 QDTPDEBUG << "invalid because findAmPm(" << sectiontext << ") returned -1";
832 break;
833 default:
834 QDTPDEBUGN("This should never happen (findAmPm returned %d)", ampm);
835 break;
836 }
837 if (result.state != Invalid)
838 m_text.replace(offset, used, sectiontext.constData(), used);
839 break; }
840 case TimeZoneSection:
841 result = findTimeZone(sectionTextRef, currentValue,
842 absoluteMax(sectionIndex),
843 absoluteMin(sectionIndex), sn.count);
844 break;
845 case MonthSection:
846 case DayOfWeekSectionShort:
847 case DayOfWeekSectionLong:
848 if (sn.count >= 3) {
849 QString sectiontext = sectionTextRef.toString();
850 int num = 0, used = 0;
851 if (sn.type == MonthSection) {
852 const QDate minDate = getMinimum(currentValue.timeRepresentation()).date();
853 const int year = currentValue.date().year(calendar);
854 const int min = (year == minDate.year(calendar)) ? minDate.month(calendar) : 1;
855 num = findMonth(sectiontext.toLower(), min, sectionIndex, year, &sectiontext, &used);
856 } else {
857 num = findDay(sectiontext.toLower(), 1, sectionIndex, &sectiontext, &used);
858 }
859
860 result = ParsedSection(Intermediate, num, used);
861 if (num != -1) {
862 m_text.replace(offset, used, sectiontext.constData(), used);
863 if (used == sectiontext.size())
864 result = ParsedSection(Acceptable, num, used);
865 }
866 break;
867 }
868 Q_FALLTHROUGH();
869 // All numeric:
870 case DaySection:
871 case YearSection:
872 case YearSection2Digits:
873 case Hour12Section:
874 case Hour24Section:
875 case MinuteSection:
876 case SecondSection:
877 case MSecSection: {
878 const auto checkSeparator = [&result, field=QStringView{m_text}.sliced(offset),
879 negativeYearOffset, sectionIndex, this]() {
880 // No-digit field if next separator is here, otherwise invalid.
881 const auto &sep = separators.at(sectionIndex + 1);
882 if (matchesSeparator(field.sliced(negativeYearOffset), sep) != -1)
883 result = ParsedSection(Intermediate, 0, negativeYearOffset);
884 else if (negativeYearOffset && matchesSeparator(field, sep) != -1)
885 result = ParsedSection(Intermediate, 0, 0);
886 else
887 return false;
888 return true;
889 };
890 int used = negativeYearOffset;
891 // We already sliced off the - sign if it was acceptable.
892 // QLocale::toUInt() would accept a sign, so we must reject it overtly:
893 if (sectionTextRef.startsWith(u'-')
894 || sectionTextRef.startsWith(u'+')) {
895 // However, a sign here may indicate a field with no digits, if it
896 // starts the next separator:
897 checkSeparator();
898 break;
899 }
900 QStringView digitsStr = sectionTextRef.left(digitCount(sectionTextRef));
901
902 if (digitsStr.isEmpty()) {
903 result = ParsedSection(Intermediate, 0, used);
904 } else {
905 const QLocale loc = locale();
906 const int absMax = absoluteMax(sectionIndex);
907 const int absMin = absoluteMin(sectionIndex);
908
909 int lastVal = -1;
910
911 for (; digitsStr.size(); digitsStr.chop(1)) {
912 bool ok = false;
913 int value = int(loc.toUInt(digitsStr, &ok));
914 if (!ok || (negate ? -value < absMin : value > absMax))
915 continue;
916
917 if (sn.type == Hour12Section) {
918 if (value > 12)
919 continue;
920 if (value == 12)
921 value = 0;
922 }
923
924 QDTPDEBUG << digitsStr << value << digitsStr.size();
925 lastVal = value;
926 used += digitsStr.size();
927 break;
928 }
929
930 if (lastVal == -1) {
931 if (!checkSeparator()) {
932 QDTPDEBUG << "invalid because" << sectionTextRef << "can't become a uint"
933 << lastVal;
934 }
935 } else {
936 if (negate)
937 lastVal = -lastVal;
938 const FieldInfo fi = fieldInfo(sectionIndex);
939 const bool unfilled = used - negativeYearOffset < sectionmaxsize;
940 if (unfilled && fi & Fraction) { // typing 2 in a zzz field should be .200, not .002
941 for (int i = used; i < sectionmaxsize; ++i)
942 lastVal *= 10;
943 }
944 // Even those *= 10s can't take last above absMax:
945 Q_ASSERT(negate ? lastVal >= absMin : lastVal <= absMax);
946 if (negate ? lastVal > absMax : lastVal < absMin) {
947 if (unfilled) {
948 result = ParsedSection(Intermediate, lastVal, used);
949 } else if (negate) {
950 QDTPDEBUG << "invalid because" << lastVal << "is greater than absoluteMax"
951 << absMax;
952 } else {
953 QDTPDEBUG << "invalid because" << lastVal << "is less than absoluteMin"
954 << absMin;
955 }
956
957 } else if (unfilled && (fi & (FixedWidth | Numeric)) == (FixedWidth | Numeric)) {
958 if (skipToNextSection(sectionIndex, currentValue, digitsStr)) {
959 const int missingZeroes = sectionmaxsize - digitsStr.size();
960 result = ParsedSection(Acceptable, lastVal, sectionmaxsize, missingZeroes);
961 m_text.insert(offset, QString(missingZeroes, u'0'));
962 ++(const_cast<QDateTimeParser*>(this)->sectionNodes[sectionIndex].zeroesAdded);
963 } else {
964 result = ParsedSection(Intermediate, lastVal, used);
965 }
966 } else if (!lastVal && !calendar.hasYearZero()
967 && (sn.type == YearSection
968 || (sn.type == YearSection2Digits && currentValue.isValid()
969 && currentValue.date().year(calendar) / 100 == 0))) {
970 // Year zero prohibited
971 result = ParsedSection(unfilled ? Acceptable : Invalid, lastVal, used);
972 } else {
973 result = ParsedSection(Acceptable, lastVal, used);
974 }
975 }
976 }
977 break; }
978 default:
979 qWarning("QDateTimeParser::parseSection Internal error (%ls %d)",
980 qUtf16Printable(sn.name()), sectionIndex);
981 return result;
982 }
983 Q_ASSERT(result.state != Invalid || result.value == -1);
984
985 return result;
986}
987
988/*!
989 \internal
990
991 Returns the day-number of a day, as close as possible to the given \a day, in
992 the specified \a month of \a year for the given \a calendar, that falls on the
993 day of the week indicated by \a weekDay.
994*/
995
996static int weekDayWithinMonth(QCalendar calendar, int year, int month, int day, int weekDay)
997{
998 // TODO: can we adapt this to cope gracefully with intercalary days (day of
999 // week > 7) without making it slower for more widely-used calendars ?
1000 const int maxDay = calendar.daysInMonth(month, year); // 0 if no such month
1001 day = maxDay > 1 ? qBound(1, day, maxDay) : qMax(1, day);
1002 day += dayOfWeekDiff(weekDay, calendar.dayOfWeek(QDate(year, month, day, calendar)));
1003 return day <= 0 ? day + 7 : maxDay > 0 && day > maxDay ? day - 7 : day;
1004}
1005
1006/*!
1007 \internal
1008 Returns whichever of baseYear through baseYear + 99 has its % 100 == y2d.
1009*/
1010static int yearInCenturyFrom(int y2d, int baseYear)
1011{
1012 Q_ASSERT(0 <= y2d && y2d < 100);
1013 const int year = baseYear - baseYear % 100 + y2d;
1014 return year < baseYear ? year + 100 : year;
1015}
1016
1017/*!
1018 \internal
1019
1020 Returns a date consistent with the given data on parts specified by known,
1021 while staying as close to the given data as it can. Returns an invalid date
1022 when on valid date is consistent with the data.
1023*/
1024
1025static QDate actualDate(QDateTimeParser::Sections known, QCalendar calendar, int baseYear,
1026 int year, int year2digits, int month, int day, int dayofweek)
1027{
1028 QDate actual(year, month, day, calendar);
1029 if (actual.isValid() && year % 100 == year2digits && calendar.dayOfWeek(actual) == dayofweek)
1030 return actual; // The obvious candidate is fine :-)
1031
1032 if (dayofweek < 1 || dayofweek > 7) // Intercalary (or invalid): ignore
1033 known &= ~QDateTimeParser::DayOfWeekSectionMask;
1034
1035 // Assuming year > 0 ...
1036 if (year % 100 != year2digits) {
1037 if (known & QDateTimeParser::YearSection2Digits) {
1038 // Year and its last two digits disagree: prefer the two-digit
1039 // reading. Not the final answer: scanString() flags the conflict
1040 // and adopts this only when the two-digit section is being edited.
1041 year = yearInCenturyFrom(year2digits, baseYear);
1042 known &= ~QDateTimeParser::YearSection;
1043 } else {
1044 year2digits = year % 100;
1045 }
1046 }
1047 Q_ASSERT(year % 100 == year2digits);
1048
1049 if (month < 1) { // If invalid, clip to nearest valid and ignore in known.
1050 month = 1;
1051 known &= ~QDateTimeParser::MonthSection;
1052 } else if (month > 12) {
1053 month = 12;
1054 known &= ~QDateTimeParser::MonthSection;
1055 }
1056 if (!actual.isValid() && !known.testAnyFlag(QDateTimeParser::YearSectionMask)
1057 && known.testFlags(QDateTimeParser::DaySection | QDateTimeParser::MonthSection)
1058 && !calendar.isLeapYear(year) && day > calendar.daysInMonth(month, year)) {
1059 // See if a leap year works better:
1060 int leap = year + 1, stop = year + 47;
1061 // (Sweden's 1700 plan (abandoned part way through) for Julian-Gregorian
1062 // transition implied no leap year after 1697 until 1744.)
1063 while (!calendar.isLeapYear(leap) && leap < stop)
1064 ++leap;
1065 if (day <= calendar.daysInMonth(month, leap))
1066 year = leap;
1067 }
1068
1069 QDate first(year, month, 1, calendar);
1070 int last = known & QDateTimeParser::MonthSection
1071 ? (known.testAnyFlag(QDateTimeParser::YearSectionMask)
1072 ? calendar.daysInMonth(month, year) : calendar.daysInMonth(month))
1073 : 0;
1074 // We can only fix DOW if we know year as well as month (hence last):
1075 const bool fixDayOfWeek = last && known & QDateTimeParser::YearSection
1076 && known & QDateTimeParser::DayOfWeekSectionMask;
1077 // If we also know day-of-week, tweak last to the last in the month that matches it:
1078 if (fixDayOfWeek) {
1079 const int diff = (dayofweek - calendar.dayOfWeek(first) - last) % 7;
1080 Q_ASSERT(diff <= 0); // C++11 specifies (-ve) % (+ve) to be <= 0.
1081 last += diff;
1082 }
1083 if (day < 1) {
1084 if (fixDayOfWeek) {
1085 day = 1 + dayofweek - calendar.dayOfWeek(first);
1086 if (day < 1)
1087 day += 7;
1088 } else {
1089 day = 1;
1090 }
1091 known &= ~QDateTimeParser::DaySection;
1092 } else if (day > calendar.maximumDaysInMonth()) {
1093 day = last;
1094 known &= ~QDateTimeParser::DaySection;
1095 } else if (last && day > last && (known & QDateTimeParser::DaySection) == 0) {
1096 day = last;
1097 }
1098
1099 actual = QDate(year, month, day, calendar);
1100 if (!actual.isValid() // We can't do better than we have, in this case
1101 || (known & QDateTimeParser::DaySection
1102 && known & QDateTimeParser::MonthSection
1103 && known & QDateTimeParser::YearSection) // ditto
1104 || calendar.dayOfWeek(actual) == dayofweek // Good enough, use it.
1105 || (known & QDateTimeParser::DayOfWeekSectionMask) == 0) { // No contradiction, use it.
1106 return actual;
1107 }
1108
1109 /*
1110 Now it gets trickier.
1111
1112 We have some inconsistency in our data; we've been told day of week, but
1113 it doesn't fit with our year, month and day. At least one of these is
1114 unknown, though: so we can fix day of week by tweaking it.
1115 */
1116
1117 if ((known & QDateTimeParser::DaySection) == 0) {
1118 // Relatively easy to fix.
1119 day = weekDayWithinMonth(calendar, year, month, day, dayofweek);
1120 actual = QDate(year, month, day, calendar);
1121 return actual;
1122 }
1123
1124 if ((known & QDateTimeParser::MonthSection) == 0) {
1125 /*
1126 Try possible month-offsets, m, preferring small; at least one (present
1127 month doesn't work) and at most 11 (max month, 12, minus min, 1); try
1128 in both directions, ignoring any offset that takes us out of range.
1129 */
1130 for (int m = 1; m < 12; m++) {
1131 if (m < month) {
1132 actual = QDate(year, month - m, day, calendar);
1133 if (calendar.dayOfWeek(actual) == dayofweek)
1134 return actual;
1135 }
1136 if (m + month <= 12) {
1137 actual = QDate(year, month + m, day, calendar);
1138 if (calendar.dayOfWeek(actual) == dayofweek)
1139 return actual;
1140 }
1141 }
1142 // Should only get here in corner cases; e.g. day == 31
1143 actual = QDate(year, month, day, calendar); // Restore from trial values.
1144 }
1145
1146 if ((known & QDateTimeParser::YearSection) == 0) {
1147 if (known & QDateTimeParser::YearSection2Digits) {
1148 actual = calendar.matchCenturyToWeekday({year, month, day}, dayofweek);
1149 if (actual.isValid()) {
1150 Q_ASSERT(calendar.dayOfWeek(actual) == dayofweek);
1151 return actual;
1152 }
1153 } else {
1154 // Offset by 7 is usually enough, but rare cases may need more:
1155 for (int y = 1; y < 12; y++) {
1156 actual = QDate(year - y, month, day, calendar);
1157 if (calendar.dayOfWeek(actual) == dayofweek)
1158 return actual;
1159 actual = QDate(year + y, month, day, calendar);
1160 if (calendar.dayOfWeek(actual) == dayofweek)
1161 return actual;
1162 }
1163 }
1164 actual = QDate(year, month, day, calendar); // Restore from trial values.
1165 }
1166
1167 return actual; // It'll just have to do :-(
1168}
1169
1170/*!
1171 \internal
1172*/
1173
1174static QTime actualTime(QDateTimeParser::Sections known,
1175 int hour, int hour12, int ampm,
1176 int minute, int second, int msec)
1177{
1178 // If we have no conflict, or don't know enough to diagonose one, use this:
1179 QTime actual(hour, minute, second, msec);
1180 if (hour12 < 0 || hour12 > 12) { // ignore bogus value
1181 known &= ~QDateTimeParser::Hour12Section;
1182 hour12 = hour % 12;
1183 }
1184
1185 if (ampm == -1 || (known & QDateTimeParser::AmPmSection) == 0) {
1186 if ((known & QDateTimeParser::Hour12Section) == 0 || hour % 12 == hour12)
1187 return actual;
1188
1189 if ((known & QDateTimeParser::Hour24Section) == 0)
1190 hour = hour12 + (hour > 12 ? 12 : 0);
1191 } else {
1192 Q_ASSERT(ampm == 0 || ampm == 1);
1193 if (hour - hour12 == ampm * 12)
1194 return actual;
1195
1196 if ((known & QDateTimeParser::Hour24Section) == 0
1197 && known & QDateTimeParser::Hour12Section) {
1198 hour = hour12 + ampm * 12;
1199 }
1200 }
1201 actual = QTime(hour, minute, second, msec);
1202 return actual;
1203}
1204
1205/*
1206 \internal
1207*/
1208static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, const QLocale &locale)
1209{
1210 // Pick longest match that we might get.
1211 qsizetype longest = 0;
1212 // On MS-Win, at least when system zone is UTC, the tzname[]s may be empty.
1213 for (int i = 0; i < 2; ++i) {
1214 const QString zone(qTzName(i));
1215 if (zone.size() > longest && name.startsWith(zone))
1216 longest = zone.size();
1217 }
1218 // Mimic each candidate QLocale::toString() could have used, to ensure round-trips work:
1219 const auto consider = [name, &longest](QStringView zone) {
1220 if (name.startsWith(zone)) {
1221 // UTC-based zone's displayName() only includes seconds if non-zero:
1222 if (9 > longest && zone.size() == 6 && zone.startsWith("UTC"_L1)
1223 && name.sliced(6, 3) == ":00"_L1) {
1224 longest = 9;
1225 } else if (zone.size() > longest) {
1226 longest = zone.size();
1227 }
1228 }
1229 };
1230#if QT_CONFIG(timezone)
1231 /* QLocale::toString would skip this if locale == QLocale::system(), but we
1232 might not be using the same system locale as whoever generated the text
1233 we're parsing. So consider it anyway. */
1234 {
1235 const auto localWhen = QDateTime(when.date(), when.time());
1236 consider(localWhen.timeRepresentation().displayName(
1237 localWhen, QTimeZone::ShortName, locale));
1238 }
1239#else
1240 Q_UNUSED(locale);
1241#endif
1242 consider(QDateTime(when.date(), when.time()).timeZoneAbbreviation());
1243 Q_ASSERT(longest <= INT_MAX); // Timezone names are not that long.
1244 return int(longest);
1245}
1246
1247#if QT_CONFIG(timezone)
1248static auto findZoneByLongName(QStringView str, const QLocale &locale, const QDateTime &when)
1249{
1250 struct R
1251 {
1252 QTimeZone zone;
1253 qsizetype nameLength = 0;
1254 bool isValid() const { return nameLength > 0 && zone.isValid(); }
1255 } result;
1256 auto pfx = QTimeZonePrivate::findLongNamePrefix(str, locale, when.toMSecsSinceEpoch());
1257 if (!pfx) // Incomplete data in when: try without time-point.
1258 pfx = QTimeZonePrivate::findLongNamePrefix(str, locale);
1259 // (We don't want offset format to match 'tttt', so do need to limit this.)
1260 // The final fall-back for QTZL's localeName() is a zoneOffsetFormat(,,NarrowFormat,,):
1261 if (!pfx)
1262 pfx = QTimeZonePrivate::findNarrowOffsetPrefix(str, locale);
1263 if (!pfx)
1264 pfx = QTimeZonePrivate::findLongUtcPrefix(str);
1265 if (pfx) {
1266 // QUtcTZP accepts texts representing offsets in a wider range than
1267 // QTZ::fromSecondsAheadOfutc() accepts:
1268 const bool niceOffset = pfx.offset
1269 && *pfx.offset >= QTimeZone::MinUtcOffsetSecs
1270 && *pfx.offset <= QTimeZone::MaxUtcOffsetSecs;
1271 result = R{ niceOffset
1272 ? QTimeZone::fromSecondsAheadOfUtc(*pfx.offset)
1273 : QTimeZone(pfx.ianaId),
1274 pfx.nameLength };
1275 Q_ASSERT(result.zone.isValid());
1276 // TODO: we should be able to take pfx.timeType into account.
1277 }
1278 return result;
1279}
1280#endif // timezone
1281
1282/*!
1283 \internal
1284*/
1285QDateTimeParser::StateNode
1286QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
1287{
1288 State state = Acceptable;
1289 bool conflicts = false;
1290 const int sectionNodesCount = sectionNodes.size();
1291 int padding = 0;
1292 int pos = 0;
1293 int year, month, day;
1294 const QDate defaultDate = defaultValue.date();
1295 const QTime defaultTime = defaultValue.time();
1296 defaultDate.getDate(&year, &month, &day);
1297 int year2digits = year % 100;
1298 int hour = defaultTime.hour();
1299 int hour12 = -1;
1300 int minute = defaultTime.minute();
1301 int second = defaultTime.second();
1302 int msec = defaultTime.msec();
1303 int dayofweek = calendar.dayOfWeek(defaultDate);
1304 QTimeZone timeZone = defaultValue.timeRepresentation();
1305
1306 int ampm = -1;
1307 Sections isSet = NoSection;
1308
1309 for (int index = 0; index < sectionNodesCount; ++index) {
1310 Q_ASSERT(state != Invalid);
1311 const QString &separator = separators.at(index);
1312 int step = matchesSeparator(QStringView{m_text}.sliced(pos), separator);
1313 if (step == -1) {
1314 QDTPDEBUG << "invalid because" << QStringView{m_text}.sliced(pos)
1315 << "does not start with" << separator
1316 << index << pos << currentSectionIndex;
1317 return StateNode();
1318 }
1319 pos += step;
1320 sectionNodes[index].pos = pos;
1321 int *current = nullptr;
1322 int zoneOffset = 0; // Needed to serve as *current when setting zone
1323 const SectionNode sn = sectionNodes.at(index);
1324 const QDateTime usedDateTime = [&] {
1325 const QDate date = actualDate(isSet, calendar, defaultCenturyStart,
1326 year, year2digits, month, day, dayofweek);
1327 const QTime time = actualTime(isSet, hour, hour12, ampm, minute, second, msec);
1328 return QDateTime(date, time, timeZone);
1329 }();
1330 ParsedSection sect = parseSection(usedDateTime, index, pos);
1331
1332 QDTPDEBUG << "sectionValue" << sn.name() << m_text
1333 << "pos" << pos << "used" << sect.used << stateName(sect.state);
1334
1335 padding += sect.zeroes;
1336 if (fixup && sect.state == Intermediate && sect.used < sn.count) {
1337 const FieldInfo fi = fieldInfo(index);
1338 if ((fi & (Numeric|FixedWidth)) == (Numeric|FixedWidth)) {
1339 const QString newText = QString::asprintf("%0*d", sn.count, sect.value);
1340 m_text.replace(pos, sect.used, newText);
1341 sect.used = sn.count;
1342 }
1343 }
1344
1345 state = qMin<State>(state, sect.state);
1346 // QDateTimeEdit can fix Intermediate and zeroes, but input needing that didn't match format:
1347 if (state == Invalid || (context == FromString && (state == Intermediate || sect.zeroes)))
1348 return StateNode();
1349
1350 switch (sn.type) {
1351 case TimeZoneSection:
1352 current = &zoneOffset;
1353 if (sect.used > 0) {
1354 // Synchronize with what findTimeZone() found:
1355 QStringView zoneName = QStringView{m_text}.sliced(pos, sect.used);
1356 Q_ASSERT(!zoneName.isEmpty()); // sect.used > 0
1357
1358 const QStringView offsetStr
1359 = zoneName.startsWith("UTC"_L1) ? zoneName.sliced(3) : zoneName;
1360 const bool isUtcOffset = offsetStr.startsWith(u'+') || offsetStr.startsWith(u'-');
1361 const bool isUtc = zoneName == "Z"_L1 || zoneName == "UTC"_L1;
1362
1363 if (isUtc || isUtcOffset) {
1364 timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
1365#if QT_CONFIG(timezone)
1366 } else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
1367 if (QTimeZone namedZone = QTimeZone(zoneName.toLatin1()); namedZone.isValid()) {
1368 timeZone = namedZone;
1369 } else {
1370 auto found = findZoneByLongName(zoneName, locale(), usedDateTime);
1371 Q_ASSERT(found.isValid());
1372 Q_ASSERT(found.nameLength == zoneName.length());
1373 timeZone = found.zone;
1374 }
1375#endif
1376 } else {
1377 timeZone = QTimeZone::LocalTime;
1378 }
1379 }
1380 break;
1381 case Hour24Section: current = &hour; break;
1382 case Hour12Section: current = &hour12; break;
1383 case MinuteSection: current = &minute; break;
1384 case SecondSection: current = &second; break;
1385 case MSecSection: current = &msec; break;
1386 case YearSection: current = &year; break;
1387 case YearSection2Digits: current = &year2digits; break;
1388 case MonthSection: current = &month; break;
1389 case DayOfWeekSectionShort:
1390 case DayOfWeekSectionLong: current = &dayofweek; break;
1391 case DaySection: current = &day; sect.value = qMax<int>(1, sect.value); break;
1392 case AmPmSection: current = &ampm; break;
1393 default:
1394 qWarning("QDateTimeParser::parse Internal error (%ls)",
1395 qUtf16Printable(sn.name()));
1396 return StateNode();
1397 }
1398 Q_ASSERT(current);
1399 Q_ASSERT(sect.state != Invalid);
1400
1401 if (sect.used > 0)
1402 pos += sect.used;
1403 QDTPDEBUG << index << sn.name() << "is set to"
1404 << pos << "state is" << stateName(state);
1405
1406 if (isSet & sn.type && *current != sect.value) {
1407 QDTPDEBUG << "CONFLICT " << sn.name() << *current << sect.value;
1408 conflicts = true;
1409 if (index != currentSectionIndex)
1410 continue;
1411 }
1412 *current = sect.value;
1413
1414 // Record the present section:
1415 isSet |= sn.type;
1416 }
1417
1418 int step = matchesSeparator(QStringView{m_text}.sliced(pos), separators.last());
1419 if (step == -1 || step + pos < m_text.size()) {
1420 QDTPDEBUG << "invalid because" << QStringView{m_text}.sliced(pos)
1421 << "does not match" << separators.last() << pos;
1422 return StateNode();
1423 }
1424
1425 if (parserType != QMetaType::QTime) {
1426 if (year % 100 != year2digits && (isSet & YearSection2Digits)) {
1427 const QDate date = actualDate(isSet, calendar, defaultCenturyStart,
1428 year, year2digits, month, day, dayofweek);
1429 if (!date.isValid()) {
1430 state = Invalid;
1431 } else if (!(isSet & YearSection)) {
1432 year = date.year(calendar);
1433 } else {
1434 conflicts = true;
1435 const SectionNode &sn = sectionNode(currentSectionIndex);
1436 if (sn.type == YearSection2Digits)
1437 year = date.year(calendar);
1438 }
1439 }
1440
1441 const auto fieldType = sectionType(currentSectionIndex);
1442 const QDate date(year, month, day, calendar);
1443 if ((!date.isValid() || dayofweek != calendar.dayOfWeek(date))
1444 && state == Acceptable && isSet & DayOfWeekSectionMask) {
1445 if (isSet & DaySection)
1446 conflicts = true;
1447 // Change to day of week should adjust day of month;
1448 // when day of month isn't set, so should change to year or month.
1449 if (currentSectionIndex == -1 || fieldType & DayOfWeekSectionMask
1450 || (!conflicts && (fieldType & (YearSectionMask | MonthSection)))) {
1451 day = weekDayWithinMonth(calendar, year, month, day, dayofweek);
1452 QDTPDEBUG << year << month << day << dayofweek
1453 << calendar.dayOfWeek(QDate(year, month, day, calendar));
1454 }
1455 }
1456
1457 bool needfixday = false;
1458 if (fieldType & DaySectionMask) {
1459 cachedDay = day;
1460 } else if (cachedDay > day && !(isSet & DayOfWeekSectionMask && state == Acceptable)) {
1461 day = cachedDay;
1462 needfixday = true;
1463 }
1464
1465 if (!calendar.isDateValid(year, month, day)) {
1466 if (day <= calendar.maximumDaysInMonth())
1467 cachedDay = day;
1468 if (day > calendar.minimumDaysInMonth() && calendar.isDateValid(year, month, 1))
1469 needfixday = true;
1470 }
1471 if (needfixday) {
1472 if (context == FromString)
1473 return StateNode();
1474 if (state == Acceptable && fixday) {
1475 day = qMin<int>(day, calendar.daysInMonth(month, year));
1476
1477 const QLocale loc = locale();
1478 for (int i=0; i<sectionNodesCount; ++i) {
1479 const SectionNode sn = sectionNode(i);
1480 if (sn.type & DaySection) {
1481 m_text.replace(sectionPos(sn), sectionSize(i), loc.toString(day));
1482 } else if (sn.type & DayOfWeekSectionMask) {
1483 const int dayOfWeek = calendar.dayOfWeek(QDate(year, month, day, calendar));
1484 const QLocale::FormatType dayFormat =
1485 (sn.type == DayOfWeekSectionShort
1486 ? QLocale::ShortFormat : QLocale::LongFormat);
1487 const QString dayName(loc.dayName(dayOfWeek, dayFormat));
1488 m_text.replace(sectionPos(sn), sectionSize(i), dayName);
1489 }
1490 }
1491 } else if (state > Intermediate) {
1492 state = Intermediate;
1493 }
1494 }
1495 }
1496
1497 if (parserType != QMetaType::QDate) {
1498 if (isSet & Hour12Section) {
1499 const bool hasHour = isSet.testAnyFlag(Hour24Section);
1500 if (ampm == -1) // If we don't know from hour, assume am:
1501 ampm = !hasHour || hour < 12 ? 0 : 1;
1502 hour12 = hour12 % 12 + ampm * 12;
1503 if (!hasHour)
1504 hour = hour12;
1505 else if (hour != hour12)
1506 conflicts = true;
1507 } else if (ampm != -1) {
1508 if (!(isSet & (Hour24Section)))
1509 hour = 12 * ampm; // Special case: only ap section
1510 else if ((ampm == 0) != (hour < 12))
1511 conflicts = true;
1512 }
1513 }
1514
1515 QDTPDEBUG << year << month << day << hour << minute << second << msec;
1516 Q_ASSERT(state != Invalid);
1517
1518 const QDate date(year, month, day, calendar);
1519 const QTime time(hour, minute, second, msec);
1520 const QDateTime when = QDateTime(date, time, timeZone);
1521
1522 if (when.time() != time || when.date() != date) {
1523 // In a spring-forward, if we hit the skipped hour, we may have been
1524 // shunted out of it.
1525
1526 // If hour wasn't specified, so we're using our default, changing it may
1527 // fix that.
1528 if (!(isSet & HourSectionMask)) {
1529 switch (parserType) {
1530 case QMetaType::QDateTime: {
1531 qint64 msecs = when.toMSecsSinceEpoch();
1532 // Fortunately, that gets a useful answer, even though when is invalid ...
1533 const QDateTime replace = QDateTime::fromMSecsSinceEpoch(msecs, timeZone);
1534 const QTime tick = replace.time();
1535 if (replace.date() == date
1536 && (!(isSet & MinuteSection) || tick.minute() == minute)
1537 && (!(isSet & SecondSection) || tick.second() == second)
1538 && (!(isSet & MSecSection) || tick.msec() == msec)) {
1539 return StateNode(replace, state, padding, conflicts);
1540 }
1541 } break;
1542 case QMetaType::QDate:
1543 // Don't care about time, so just use start of day (and ignore spec):
1544 return StateNode(date.startOfDay(QTimeZone::UTC),
1545 state, padding, conflicts);
1546 break;
1547 case QMetaType::QTime:
1548 // Don't care about date or representation, so pick a safe representation:
1549 return StateNode(QDateTime(date, time, QTimeZone::UTC),
1550 state, padding, conflicts);
1551 default:
1552 Q_UNREACHABLE_RETURN(StateNode());
1553 }
1554 } else if (state > Intermediate) {
1555 state = Intermediate;
1556 }
1557 }
1558
1559 return StateNode(when, state, padding, conflicts);
1560}
1561
1562/*!
1563 \internal
1564*/
1565
1566QDateTimeParser::StateNode
1567QDateTimeParser::parse(const QString &input, int position,
1568 const QDateTime &defaultValue, bool fixup) const
1569{
1570 const QDateTime minimum = getMinimum(defaultValue.timeRepresentation());
1571 const QDateTime maximum = getMaximum(defaultValue.timeRepresentation());
1572 m_text = input;
1573
1574 QDTPDEBUG << "parse" << input;
1575 StateNode scan = scanString(defaultValue, fixup);
1576 QDTPDEBUGN("'%s' => '%s'(%s)", m_text.toLatin1().constData(),
1577 scan.value.toString("yyyy/MM/dd hh:mm:ss.zzz"_L1).toLatin1().constData(),
1578 stateName(scan.state).toLatin1().constData());
1579
1580 if (scan.value.isValid() && scan.state != Invalid) {
1581 if (context != FromString && scan.value < minimum) {
1582 bool done = false;
1583 scan.state = Invalid;
1584 const int sectionNodesCount = sectionNodes.size();
1585 for (int i=0; i<sectionNodesCount && !done; ++i) {
1586 const SectionNode &sn = sectionNodes.at(i);
1587 QString t = sectionText(m_text, i, sn.pos).toLower();
1588 if ((t.size() < sectionMaxSize(i)
1589 && ((fieldInfo(i) & (FixedWidth|Numeric)) != Numeric))
1590 || t.contains(u' ')) {
1591 switch (sn.type) {
1592 case TimeZoneSection:
1593 // Don't even try to find a zone that might match if we
1594 // amended the text. It may well exist but it'd be
1595 // insanely expensive to check.
1596 break;
1597 case DayOfWeekSectionShort:
1598 case DayOfWeekSectionLong:
1599 // Non-numeric field, no point trying to fix up by
1600 // adding digits. (Could do something similar to
1601 // MonthSection, for count >= 3, but we've got by
1602 // without it thus far.)
1603 scan.state = Invalid;
1604 done = true;
1605 break;
1606 case AmPmSection:
1607 switch (findAmPm(t, i)) {
1608 case AM:
1609 case PM:
1610 scan.state = Acceptable;
1611 done = true;
1612 break;
1613 case Neither:
1614 scan.state = Invalid;
1615 done = true;
1616 break;
1617 case PossibleAM:
1618 case PossiblePM:
1619 case PossibleBoth: {
1620 const QDateTime copy(scan.value.addSecs(12 * 60 * 60));
1621 if (copy >= minimum && copy <= maximum) {
1622 scan.state = Intermediate;
1623 done = true;
1624 }
1625 break; }
1626 }
1627 Q_FALLTHROUGH();
1628 case MonthSection:
1629 if (sn.count >= 3) {
1630 const QDate when = scan.value.date();
1631 const int finalMonth = when.month(calendar);
1632 int tmp = finalMonth;
1633 // I know the first possible month makes the date too early
1634 while ((tmp = findMonth(t, tmp + 1, i, when.year(calendar))) != -1) {
1635 const QDateTime copy(scan.value.addMonths(tmp - finalMonth));
1636 if (copy >= minimum && copy <= maximum)
1637 break; // break out of while
1638 }
1639 if (tmp != -1) {
1640 scan.state = Intermediate;
1641 done = true;
1642 }
1643 break;
1644 }
1645 Q_FALLTHROUGH();
1646 default: {
1647 // Numeric field: could we fix up by inserting zeros
1648 // somewhere in it ? If so, Intermediate, else Invalid.
1649 int toMin;
1650 int toMax;
1651
1652 if (sn.type & TimeSectionMask) {
1653 if (scan.value.daysTo(minimum) != 0)
1654 break;
1655
1656 const QTime time = scan.value.time();
1657 toMin = time.msecsTo(minimum.time());
1658 if (scan.value.daysTo(maximum) > 0)
1659 toMax = -1; // can't get to max
1660 else
1661 toMax = time.msecsTo(maximum.time());
1662 } else {
1663 toMin = scan.value.daysTo(minimum);
1664 toMax = scan.value.daysTo(maximum);
1665 }
1666 const int maxChange = sn.maxChange();
1667 if (toMin > maxChange) {
1668 QDTPDEBUG << "invalid because toMin > maxChange" << toMin
1669 << maxChange << t << scan.value << minimum;
1670 scan.state = Invalid;
1671 done = true;
1672 break;
1673 } else if (toMax > maxChange) {
1674 toMax = -1; // can't get to max
1675 }
1676
1677 const int min = getDigit(minimum, i);
1678 if (min == -1) {
1679 qWarning("QDateTimeParser::parse Internal error 4 (%ls)",
1680 qUtf16Printable(sn.name()));
1681 scan.state = Invalid;
1682 done = true;
1683 break;
1684 }
1685
1686 int max = toMax != -1 ? getDigit(maximum, i) : absoluteMax(i, scan.value);
1687 int pos = position + scan.padded - sn.pos;
1688 if (pos < 0 || pos >= t.size())
1689 pos = -1;
1690 if (!potentialValue(t.simplified(), min, max, i, scan.value, pos)) {
1691 QDTPDEBUG << "invalid because potentialValue(" << t.simplified() << min << max
1692 << sn.name() << "returned" << toMax << toMin << pos;
1693 scan.state = Invalid;
1694 done = true;
1695 break;
1696 }
1697 scan.state = Intermediate;
1698 done = true;
1699 break; }
1700 }
1701 }
1702 }
1703 } else {
1704 if (scan.value > maximum)
1705 scan.state = Invalid;
1706
1707 QDTPDEBUG << "not checking intermediate because scanned value is"
1708 << scan.value << minimum << maximum;
1709 }
1710 }
1711
1712 // An invalid time should only arise if we set the state to less than acceptable:
1713 Q_ASSERT(scan.value.isValid() || scan.state != Acceptable);
1714
1715 return scan;
1716}
1717
1718/*
1719 \internal
1720 \brief Returns the index in \a entries with the best prefix match to \a text
1721
1722 Scans \a entries looking for an entry overlapping \a text as much as possible
1723 (an exact match beats any prefix match; a match of the full entry as prefix of
1724 text beats any entry but one matching a longer prefix; otherwise, the match of
1725 longest prefix wins, earlier entries beating later on a draw). Records the
1726 length of overlap in *used (if \a used is non-NULL) and the first entry that
1727 overlapped this much in *usedText (if \a usedText is non-NULL).
1728 */
1729static int findTextEntry(QStringView text, const ShortVector<QString> &entries, QString *usedText, int *used)
1730{
1731 if (text.isEmpty())
1732 return -1;
1733
1734 int bestMatch = -1;
1735 int bestCount = 0;
1736 for (int n = 0; n < entries.size(); ++n)
1737 {
1738 const QString &name = entries.at(n);
1739
1740 const int limit = qMin(text.size(), name.size());
1741 int i = 0;
1742 while (i < limit && text.at(i) == name.at(i).toLower())
1743 ++i;
1744 // Full match beats an equal prefix match:
1745 if (i > bestCount || (i == bestCount && i == name.size())) {
1746 bestCount = i;
1747 bestMatch = n;
1748 if (i == name.size() && i == text.size())
1749 break; // Exact match, name == text, wins.
1750 }
1751 }
1752 if (usedText && bestMatch != -1)
1753 *usedText = entries.at(bestMatch);
1754 if (used)
1755 *used = bestCount;
1756
1757 return bestMatch;
1758}
1759
1760/*!
1761 \internal
1762 finds the first possible monthname that \a str1 can
1763 match. Starting from \a index; str should already by lowered
1764*/
1765
1766int QDateTimeParser::findMonth(QStringView str, int startMonth, int sectionIndex,
1767 int year, QString *usedMonth, int *used) const
1768{
1769 const SectionNode &sn = sectionNode(sectionIndex);
1770 if (sn.type != MonthSection) {
1771 qWarning("QDateTimeParser::findMonth Internal error");
1772 return -1;
1773 }
1774
1775 QLocale::FormatType type = sn.count == 3 ? QLocale::ShortFormat : QLocale::LongFormat;
1776 QLocale l = locale();
1777 ShortVector<QString> monthNames;
1778 monthNames.reserve(13 - startMonth);
1779 for (int month = startMonth; month <= 12; ++month)
1780 monthNames.append(calendar.monthName(l, month, year, type));
1781
1782 const int index = findTextEntry(str, monthNames, usedMonth, used);
1783 return index < 0 ? index : index + startMonth;
1784}
1785
1786int QDateTimeParser::findDay(QStringView str, int startDay, int sectionIndex, QString *usedDay, int *used) const
1787{
1788 const SectionNode &sn = sectionNode(sectionIndex);
1789 if (!(sn.type & DaySectionMask)) {
1790 qWarning("QDateTimeParser::findDay Internal error");
1791 return -1;
1792 }
1793
1794 QLocale::FormatType type = sn.count == 4 ? QLocale::LongFormat : QLocale::ShortFormat;
1795 QLocale l = locale();
1796 ShortVector<QString> daysOfWeek;
1797 daysOfWeek.reserve(8 - startDay);
1798 for (int day = startDay; day <= 7; ++day)
1799 daysOfWeek.append(l.dayName(day, type));
1800
1801 const int index = findTextEntry(str, daysOfWeek, usedDay, used);
1802 return index < 0 ? index : index + startDay;
1803}
1804
1805/*!
1806 \internal
1807
1808 Return's .value is UTC offset in seconds.
1809 The caller must verify that the offset is within a valid range.
1810 The mode is 1 for permissive parsing, 2 and 3 for strict offset-only format
1811 (no UTC prefix) with no colon for 2 and a colon for 3.
1812 */
1813QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str, int mode) const
1814{
1815 Q_ASSERT(mode > 0 && mode < 4);
1816 const bool startsWithUtc = str.startsWith("UTC"_L1);
1817 // Deal with UTC prefix if present:
1818 if (startsWithUtc) {
1819 if (mode != 1)
1820 return ParsedSection();
1821 str = str.sliced(3);
1822 if (str.isEmpty())
1823 return ParsedSection(Acceptable, 0, 3);
1824 }
1825
1826 const bool negativeSign = str.startsWith(u'-');
1827 // Must start with a sign:
1828 if (!negativeSign && !str.startsWith(u'+'))
1829 return ParsedSection();
1830 str = str.sliced(1); // drop sign
1831
1832 const int colonPosition = str.indexOf(u':');
1833 // Colon that belongs to offset is at most at position 2 (hh:mm)
1834 bool hasColon = (colonPosition >= 0 && colonPosition < 3);
1835
1836 // We deal only with digits at this point (except ':'), so collect them
1837 const int digits = hasColon ? colonPosition + 3 : 4;
1838 int i = 0;
1839 for (const int offsetLength = qMin(qsizetype(digits), str.size()); i < offsetLength; ++i) {
1840 if (i != colonPosition && !str.at(i).isDigit())
1841 break;
1842 }
1843 const int hoursLength = qMin(i, hasColon ? colonPosition : 2);
1844 if (hoursLength < 1)
1845 return ParsedSection();
1846 // Field either ends with hours or also has two digits of minutes
1847 if (i < digits) {
1848 // Only allow single-digit hours with UTC prefix or :mm suffix
1849 if (!startsWithUtc && hoursLength != 2)
1850 return ParsedSection();
1851 i = hoursLength;
1852 hasColon = false;
1853 }
1854 if (mode == (hasColon ? 2 : 3))
1855 return ParsedSection();
1856 str.truncate(i); // The rest of the string is not part of the UTC offset
1857
1858 bool isInt = false;
1859 const int hours = str.first(hoursLength).toInt(&isInt);
1860 if (!isInt)
1861 return ParsedSection();
1862 const QStringView minutesStr = str.mid(hasColon ? colonPosition + 1 : 2, 2);
1863 const int minutes = minutesStr.isEmpty() ? 0 : minutesStr.toInt(&isInt);
1864 if (!isInt)
1865 return ParsedSection();
1866
1867 // Keep in sync with QTimeZone::maxUtcOffset hours (14 at most). Also, user
1868 // could be in the middle of updating the offset (e.g. UTC+14:23) which is
1869 // an intermediate state
1870 const State status = (hours > 14 || minutes >= 60) ? Invalid
1871 : (hours == 14 && minutes > 0) ? Intermediate : Acceptable;
1872
1873 int offset = 3600 * hours + 60 * minutes;
1874 if (negativeSign)
1875 offset = -offset;
1876
1877 // Used: UTC, sign, hours, colon, minutes
1878 const int usedSymbols = (startsWithUtc ? 3 : 0) + 1 + hoursLength + (hasColon ? 1 : 0)
1879 + minutesStr.size();
1880
1881 return ParsedSection(status, offset, usedSymbols);
1882}
1883
1884/*!
1885 \internal
1886
1887 Return's .value is zone's offset, zone time - UTC time, in seconds.
1888 The caller must verify that the offset is within a valid range.
1889 See QTimeZonePrivate::isValidId() for the format of zone names.
1890 */
1891QDateTimeParser::ParsedSection
1892QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
1893{
1894 const int systemLength = startsWithLocalTimeZone(str, when, locale());
1895#if QT_CONFIG(timezone)
1896 // Collect up plausibly-valid characters; let QTimeZone work out what's
1897 // truly valid.
1898 const auto invalidZoneNameCharacter = [] (const QChar &c) {
1899 const auto cu = c.unicode();
1900 return cu >= 127u || !(memchr("+-./:_", char(cu), 6) || c.isLetterOrNumber());
1901 };
1902 int index = std::distance(str.cbegin(),
1903 std::find_if(str.cbegin(), str.cend(), invalidZoneNameCharacter));
1904
1905 // Limit name fragments (between slashes) to 20 characters.
1906 // (Valid time-zone IDs are allowed up to 14 and Android has quirks up to 17.)
1907 // Limit number of fragments to six; no known zone name has more than four.
1908 int lastSlash = -1;
1909 int count = 0;
1910 Q_ASSERT(index <= str.size());
1911 while (lastSlash < index) {
1912 int slash = str.indexOf(u'/', lastSlash + 1);
1913 if (slash < 0 || slash > index)
1914 slash = index; // i.e. the end of the candidate text
1915 else if (++count > 5)
1916 index = slash; // Truncate
1917 if (slash - lastSlash > 20)
1918 index = lastSlash + 20; // Truncate
1919 // If any of those conditions was met, index <= slash, so this exits the loop:
1920 lastSlash = slash;
1921 }
1922
1923 // Find longest IANA ID match:
1924 for (QStringView copy = str; index > systemLength; --index) {
1925 copy.truncate(index);
1926 QTimeZone zone(copy.toLatin1());
1927 if (zone.isValid())
1928 return ParsedSection(Acceptable, zone.offsetFromUtc(when), index);
1929 }
1930 // Not a known IANA ID.
1931
1932 if (auto found = findZoneByLongName(str, locale(), when); found.isValid())
1933 return ParsedSection(Acceptable, found.zone.offsetFromUtc(when), found.nameLength);
1934#endif
1935 if (systemLength > 0) // won't actually use the offset, but need it to be valid
1936 return ParsedSection(Acceptable, when.toLocalTime().offsetFromUtc(), systemLength);
1937 return ParsedSection();
1938}
1939
1940/*!
1941 \internal
1942
1943 Return's .value is zone's offset, zone time - UTC time, in seconds.
1944 See QTimeZonePrivate::isValidId() for the format of zone names.
1945
1946 The mode is the number of 't' characters in the field specifier:
1947 * 1: any recognized format
1948 * 2: only the simple offset format, without colon
1949 * 3: only the simple offset format, with colon
1950 * 4: only a zone name
1951*/
1952QDateTimeParser::ParsedSection
1953QDateTimeParser::findTimeZone(QStringView str, const QDateTime &when,
1954 int maxVal, int minVal, int mode) const
1955{
1956 Q_ASSERT(mode > 0 && mode <= 4);
1957 // Short-cut Zulu suffix when it's all there is (rather than a prefix match):
1958 if (mode == 1 && str == u'Z')
1959 return ParsedSection(Acceptable, 0, 1);
1960
1961 ParsedSection section;
1962 if (mode != 4)
1963 section = findUtcOffset(str, mode);
1964 if (mode != 2 && mode != 3 && section.used <= 0) // if nothing used, try time zone parsing
1965 section = findTimeZoneName(str, when);
1966 // It can be a well formed time zone specifier, but with value out of range
1967 if (section.state == Acceptable && (section.value < minVal || section.value > maxVal))
1968 section.state = Intermediate;
1969 if (section.used > 0)
1970 return section;
1971
1972 if (mode == 1) {
1973 // Check if string is UTC or alias to UTC, after all other options
1974 if (str.startsWith("UTC"_L1))
1975 return ParsedSection(Acceptable, 0, 3);
1976 if (str.startsWith(u'Z'))
1977 return ParsedSection(Acceptable, 0, 1);
1978 }
1979
1980 return ParsedSection();
1981}
1982
1983/*!
1984 \internal
1985
1986 Compares str to the am/pm texts returned by getAmPmText().
1987 Returns AM or PM if str is one of those texts. Failing that, it looks to see
1988 whether, ignoring spaces and case, each character of str appears in one of
1989 the am/pm texts.
1990 If neither text can be the result of the user typing more into str, returns
1991 Neither. If both texts are possible results of further typing, returns
1992 PossibleBoth. Otherwise, only one of them is a possible completion, so this
1993 returns PossibleAM or PossiblePM to indicate which.
1994
1995 \sa getAmPmText()
1996*/
1997QDateTimeParser::AmPmFinder QDateTimeParser::findAmPm(QString &str, int sectionIndex, int *used) const
1998{
1999 const SectionNode &s = sectionNode(sectionIndex);
2000 if (s.type != AmPmSection) {
2001 qWarning("QDateTimeParser::findAmPm Internal error");
2002 return Neither;
2003 }
2004 if (used)
2005 *used = str.size();
2006 if (QStringView(str).trimmed().isEmpty())
2007 return PossibleBoth;
2008
2009 const QLatin1Char space(' ');
2010 int size = sectionMaxSize(sectionIndex);
2011
2012 enum {
2013 amindex = 0,
2014 pmindex = 1
2015 };
2016 QString ampm[2];
2017 ampm[amindex] = getAmPmText(AmText, Case(s.count));
2018 ampm[pmindex] = getAmPmText(PmText, Case(s.count));
2019 for (int i = 0; i < 2; ++i)
2020 ampm[i].truncate(size);
2021
2022 QDTPDEBUG << "findAmPm" << str << ampm[0] << ampm[1];
2023
2024 if (str.startsWith(ampm[amindex], Qt::CaseInsensitive)) {
2025 str = ampm[amindex];
2026 // str might've changed its length
2027 if (used)
2028 *used = str.size();
2029 return AM;
2030 } else if (str.startsWith(ampm[pmindex], Qt::CaseInsensitive)) {
2031 str = ampm[pmindex];
2032 // str might've changed its length
2033 if (used)
2034 *used = str.size();
2035 return PM;
2036 } else if (context == FromString || (str.count(space) == 0 && str.size() >= size)) {
2037 return Neither;
2038 }
2039 size = qMin(size, str.size());
2040
2041 bool broken[2] = {false, false};
2042 for (int i=0; i<size; ++i) {
2043 const QChar ch = str.at(i);
2044 if (ch != space) {
2045 for (int j=0; j<2; ++j) {
2046 if (!broken[j]) {
2047 int index = ampm[j].indexOf(ch);
2048 QDTPDEBUG << "looking for" << ch
2049 << "in" << ampm[j] << "and got" << index;
2050 if (index == -1) {
2051 if (ch.category() == QChar::Letter_Uppercase) {
2052 index = ampm[j].indexOf(ch.toLower());
2053 QDTPDEBUG << "trying with" << ch.toLower()
2054 << "in" << ampm[j] << "and got" << index;
2055 } else if (ch.category() == QChar::Letter_Lowercase) {
2056 index = ampm[j].indexOf(ch.toUpper());
2057 QDTPDEBUG << "trying with" << ch.toUpper()
2058 << "in" << ampm[j] << "and got" << index;
2059 }
2060 if (index == -1) {
2061 broken[j] = true;
2062 if (broken[amindex] && broken[pmindex]) {
2063 QDTPDEBUG << str << "didn't make it";
2064 return Neither;
2065 }
2066 continue;
2067 } else {
2068 str[i] = ampm[j].at(index); // fix case
2069 }
2070 }
2071 ampm[j].remove(index, 1);
2072 }
2073 }
2074 }
2075 }
2076 if (!broken[pmindex] && !broken[amindex])
2077 return PossibleBoth;
2078 return (!broken[amindex] ? PossibleAM : PossiblePM);
2079}
2080
2081/*!
2082 \internal
2083 Max number of units that can be changed by this section.
2084*/
2085
2086int QDateTimeParser::SectionNode::maxChange() const
2087{
2088 using namespace QtPrivate::DateTimeConstants;
2089 switch (type) {
2090 // Time. unit is msec
2091 case MSecSection: return MSECS_PER_SEC - 1;
2092 case SecondSection: return (SECS_PER_MIN - 1) * MSECS_PER_SEC;
2093 case MinuteSection: return (MINS_PER_HOUR - 1) * MSECS_PER_MIN;
2094 case Hour24Section: return 23 * MSECS_PER_HOUR;
2095 case Hour12Section: return 11 * MSECS_PER_HOUR;
2096 case TimeZoneSection:
2097 return (QTimeZone::MaxUtcOffsetSecs - QTimeZone::MinUtcOffsetSecs - 1) * MSECS_PER_SEC;
2098
2099 // Date. unit is day (assuming Gregorian calendar):
2100 // 1 to 7 (or n month-start to n-1 of next week, month-end to n+1 of previous week):
2101 case DayOfWeekSectionShort:
2102 case DayOfWeekSectionLong: return 6;
2103 // 1st to 31st:
2104 case DaySection: return 30;
2105 // n-th of Jan to n-th of Dec in a leap year:
2106 case MonthSection: return 366 - 31;
2107 // Years range from -9999 to +9999 with no 0 in the middle:
2108 case YearSection: return 19997 * 365 + 50 * 97 - 1;
2109 /* (-10001 and 10000 are leap years; the 200 centuries minus two years
2110 of the supported range thus only get 49 multiple-of-four centuries in
2111 the 50 Gregorian cycles they overlap.)
2112 */
2113 // Change within a century:
2114 case YearSection2Digits: return 100 * 365 + 24;
2115 // (... albeit day of week and month plus month could make it jump by more.)
2116 default:
2117 qWarning("QDateTimeParser::maxChange() Internal error (%ls)",
2118 qUtf16Printable(name()));
2119 }
2120
2121 return -1;
2122}
2123
2124QDateTimeParser::FieldInfo QDateTimeParser::fieldInfo(int index) const
2125{
2126 FieldInfo ret;
2127 const SectionNode &sn = sectionNode(index);
2128 switch (sn.type) {
2129 case MSecSection:
2130 ret |= Fraction;
2131 Q_FALLTHROUGH();
2132 case SecondSection:
2133 case MinuteSection:
2134 case Hour24Section:
2135 case Hour12Section:
2136 case YearSection2Digits:
2137 ret |= AllowPartial;
2138 Q_FALLTHROUGH();
2139 case YearSection:
2140 ret |= Numeric;
2141 if (sn.count != 1)
2142 ret |= FixedWidth;
2143 break;
2144 case MonthSection:
2145 case DaySection:
2146 switch (sn.count) {
2147 case 2:
2148 ret |= FixedWidth;
2149 Q_FALLTHROUGH();
2150 case 1:
2151 ret |= (Numeric|AllowPartial);
2152 break;
2153 }
2154 break;
2155 case DayOfWeekSectionShort:
2156 case DayOfWeekSectionLong:
2157 if (sn.count == 3)
2158 ret |= FixedWidth;
2159 break;
2160 case AmPmSection:
2161 // Some locales have different length AM and PM texts.
2162 if (getAmPmText(AmText, Case(sn.count)).size()
2163 == getAmPmText(PmText, Case(sn.count)).size()) {
2164 // Only relevant to DateTimeEdit's fixups in parse().
2165 ret |= FixedWidth;
2166 }
2167 break;
2168 case TimeZoneSection:
2169 break;
2170 default:
2171 qWarning("QDateTimeParser::fieldInfo Internal error 2 (%d %ls %d)",
2172 index, qUtf16Printable(sn.name()), sn.count);
2173 break;
2174 }
2175 return ret;
2176}
2177
2178QString QDateTimeParser::SectionNode::format() const
2179{
2180 QChar fillChar;
2181 switch (type) {
2182 case AmPmSection: return count == 1 ? "ap"_L1 : count == 2 ? "AP"_L1 : "Ap"_L1;
2183 case MSecSection: fillChar = u'z'; break;
2184 case SecondSection: fillChar = u's'; break;
2185 case MinuteSection: fillChar = u'm'; break;
2186 case Hour24Section: fillChar = u'H'; break;
2187 case Hour12Section: fillChar = u'h'; break;
2188 case DayOfWeekSectionShort:
2189 case DayOfWeekSectionLong:
2190 case DaySection: fillChar = u'd'; break;
2191 case MonthSection: fillChar = u'M'; break;
2192 case YearSection2Digits:
2193 case YearSection: fillChar = u'y'; break;
2194 default:
2195 qWarning("QDateTimeParser::sectionFormat Internal error (%ls)",
2196 qUtf16Printable(name(type)));
2197 return QString();
2198 }
2199 if (fillChar.isNull()) {
2200 qWarning("QDateTimeParser::sectionFormat Internal error 2");
2201 return QString();
2202 }
2203 return QString(count, fillChar);
2204}
2205
2206
2207/*!
2208 \internal
2209
2210 Returns \c true if str can be modified to represent a
2211 number that is within min and max.
2212*/
2213
2214bool QDateTimeParser::potentialValue(QStringView str, int min, int max, int index,
2215 const QDateTime &currentValue, int insert) const
2216{
2217 if (str.isEmpty())
2218 return true;
2219
2220 const int size = sectionMaxSize(index);
2221 int val = (int)locale().toUInt(str);
2222 const SectionNode &sn = sectionNode(index);
2223 if (sn.type == YearSection2Digits) {
2224 const int year = currentValue.date().year(calendar);
2225 val += year - (year % 100);
2226 }
2227 if (val >= min && val <= max && str.size() == size)
2228 return true;
2229 if (val > max || (str.size() == size && val < min))
2230 return false;
2231
2232 const int len = size - str.size();
2233 for (int i=0; i<len; ++i) {
2234 for (int j=0; j<10; ++j) {
2235 if (potentialValue(str + QLatin1Char('0' + j), min, max, index, currentValue, insert)) {
2236 return true;
2237 } else if (insert >= 0) {
2238 const QString tmp = str.left(insert) + QLatin1Char('0' + j) + str.mid(insert);
2239 if (potentialValue(tmp, min, max, index, currentValue, insert))
2240 return true;
2241 }
2242 }
2243 }
2244
2245 return false;
2246}
2247
2248/*!
2249 \internal
2250*/
2251bool QDateTimeParser::skipToNextSection(int index, const QDateTime &current, QStringView text) const
2252{
2253 Q_ASSERT(text.size() < sectionMaxSize(index));
2254 const SectionNode &node = sectionNode(index);
2255 int min = absoluteMin(index);
2256 int max = absoluteMax(index, current);
2257 // Time-zone field is only numeric if given as offset from UTC:
2258 if (node.type != TimeZoneSection || current.timeSpec() == Qt::OffsetFromUTC) {
2259 const QDateTime maximum = getMaximum(current.timeRepresentation());
2260 const QDateTime minimum = getMinimum(current.timeRepresentation());
2261 // Range from minimum to maximum might not contain current if an earlier
2262 // field's value was full-width but out of range. In such a case the
2263 // parse is already headed for Invalid, so it doesn't matter that we get
2264 // the wrong range of values for the current field here.
2265
2266 QDateTime tmp = current;
2267 if (!setDigit(tmp, index, min) || tmp < minimum)
2268 min = getDigit(minimum, index);
2269
2270 if (!setDigit(tmp, index, max) || tmp > maximum)
2271 max = getDigit(maximum, index);
2272 }
2273 int pos = cursorPosition() - node.pos;
2274 if (pos < 0 || pos >= text.size())
2275 pos = -1;
2276
2277 /*
2278 If the value potentially can become another valid entry we don't want to
2279 skip to the next. E.g. In a M field (month without leading 0) if you type
2280 1 we don't want to autoskip (there might be [012] following) but if you
2281 type 3 we do.
2282 */
2283 return !potentialValue(text, min, max, index, current, pos);
2284}
2285
2286/*!
2287 \internal
2288 For debugging. Returns the name of the section \a s.
2289*/
2290
2291QString QDateTimeParser::SectionNode::name(QDateTimeParser::Section s)
2292{
2293 switch (s) {
2294 case AmPmSection: return "AmPmSection"_L1;
2295 case DaySection: return "DaySection"_L1;
2296 case DayOfWeekSectionShort: return "DayOfWeekSectionShort"_L1;
2297 case DayOfWeekSectionLong: return "DayOfWeekSectionLong"_L1;
2298 case Hour24Section: return "Hour24Section"_L1;
2299 case Hour12Section: return "Hour12Section"_L1;
2300 case MSecSection: return "MSecSection"_L1;
2301 case MinuteSection: return "MinuteSection"_L1;
2302 case MonthSection: return "MonthSection"_L1;
2303 case SecondSection: return "SecondSection"_L1;
2304 case TimeZoneSection: return "TimeZoneSection"_L1;
2305 case YearSection: return "YearSection"_L1;
2306 case YearSection2Digits: return "YearSection2Digits"_L1;
2307 case NoSection: return "NoSection"_L1;
2308 case FirstSection: return "FirstSection"_L1;
2309 case LastSection: return "LastSection"_L1;
2310 default: return "Unknown section "_L1 + QString::number(int(s));
2311 }
2312}
2313
2314/*!
2315 \internal
2316 For debugging. Returns the name of the state \a s.
2317*/
2318
2319QString QDateTimeParser::stateName(State s) const
2320{
2321 switch (s) {
2322 case Invalid: return "Invalid"_L1;
2323 case Intermediate: return "Intermediate"_L1;
2324 case Acceptable: return "Acceptable"_L1;
2325 default: return "Unknown state "_L1 + QString::number(s);
2326 }
2327}
2328
2329
2330/*!
2331 \internal
2332 Compute a defaultValue to pass to parse().
2333*/
2334QDateTime QDateTimeParser::baseDate(const QTimeZone &zone) const
2335{
2336 QDateTime when = QDate(defaultCenturyStart, 1, 1).startOfDay(zone);
2337 if (const QDateTime start = getMinimum(zone); when < start)
2338 return start;
2339 if (const QDateTime end = getMaximum(zone); when > end)
2340 return end;
2341 return when;
2342}
2343
2344// Only called when we want only one of date or time; use UTC to avoid bogus DST issues.
2345bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time, int baseYear) const
2346{
2347 defaultCenturyStart = baseYear;
2348 const StateNode tmp = parse(t, -1, baseDate(QTimeZone::UTC), false);
2349 if (tmp.state != Acceptable || tmp.conflicts)
2350 return false;
2351
2352 if (time) {
2353 Q_ASSERT(!date);
2354 const QTime t = tmp.value.time();
2355 if (!t.isValid())
2356 return false;
2357 *time = t;
2358 }
2359
2360 if (date) {
2361 Q_ASSERT(!time);
2362 const QDate d = tmp.value.date();
2363 if (!d.isValid())
2364 return false;
2365 *date = d;
2366 }
2367 return true;
2368}
2369
2370// Only called when we want both date and time; default to local time.
2371bool QDateTimeParser::fromString(const QString &t, QDateTime *datetime, int baseYear) const
2372{
2373 defaultCenturyStart = baseYear;
2374 const StateNode tmp = parse(t, -1, baseDate(QTimeZone::LocalTime), false);
2375 if (datetime)
2376 *datetime = tmp.value;
2377 return tmp.state >= Intermediate && !tmp.conflicts && tmp.value.isValid();
2378}
2379
2380QDateTime QDateTimeParser::getMinimum(const QTimeZone &zone) const
2381{
2382 // NB: QDateTimeParser always uses Qt::LocalTime time spec by default. If
2383 // any subclass needs a changing time spec, it must override this
2384 // method. At the time of writing, this is done by QDateTimeEditPrivate.
2385
2386 // Cache the only case (and make sure it knows its UTC offset):
2387 static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN.startOfDay());
2388 static const QDateTime utcTimeMin = localTimeMin.toUTC();
2389 switch (zone.timeSpec()) {
2390 case Qt::LocalTime:
2391 return localTimeMin;
2392 case Qt::UTC:
2393 return utcTimeMin;
2394 case Qt::OffsetFromUTC:
2395 case Qt::TimeZone:
2396 break;
2397 }
2398 return utcTimeMin.toTimeZone(zone);
2399}
2400
2401QDateTime QDateTimeParser::getMaximum(const QTimeZone &zone) const
2402{
2403 // NB: QDateTimeParser always uses Qt::LocalTime time spec by default. If
2404 // any subclass needs a changing time spec, it must override this
2405 // method. At the time of writing, this is done by QDateTimeEditPrivate.
2406
2407 // Cache the only case
2408 static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay());
2409 static const QDateTime utcTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay(QTimeZone::UTC));
2410 switch (zone.timeSpec()) {
2411 case Qt::LocalTime:
2412 return localTimeMax;
2413 case Qt::UTC:
2414 return utcTimeMax;
2415 case Qt::OffsetFromUTC:
2416 case Qt::TimeZone:
2417 break;
2418 }
2419 return utcTimeMax.toTimeZone(zone);
2420}
2421
2422QString QDateTimeParser::getAmPmText(AmPm ap, Case cs) const
2423{
2424 const QLocale loc = locale();
2425 QString raw = ap == AmText ? loc.amText() : loc.pmText();
2426 switch (cs)
2427 {
2428 case UpperCase: return std::move(raw).toUpper();
2429 case LowerCase: return std::move(raw).toLower();
2430 case NativeCase: return raw;
2431 }
2432 Q_UNREACHABLE_RETURN(raw);
2433}
2434
2435/*
2436 \internal
2437*/
2438
2439bool operator==(QDateTimeParser::SectionNode s1, QDateTimeParser::SectionNode s2)
2440{
2441 return (s1.type == s2.type) && (s1.pos == s2.pos) && (s1.count == s2.count);
2442}
2443
2444/*!
2445 \internal
2446 Sets \a cal as the calendar to use. The default is Gregorian.
2447*/
2448
2449void QDateTimeParser::setCalendar(QCalendar cal)
2450{
2451 calendar = cal;
2452}
2453
2454QT_END_NAMESPACE
Combined button and popup list for selecting options.
static int dayOfWeekDiff(int sought, int held)
static void appendSeparator(QStringList *list, QStringView string, int from, int size, int lastQuote)
static int countRepeat(QStringView str, int index, int maxCount)
static int weekDayWithinMonth(QCalendar calendar, int year, int month, int day, int weekDay)
static QString unquote(QStringView str)
static int yearInCenturyFrom(int y2d, int baseYear)
static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, const QLocale &locale)
static bool preferDayOfWeek(const QList< QDateTimeParser::SectionNode > &nodes)
#define QDTPDEBUGN
bool operator==(QDateTimeParser::SectionNode s1, QDateTimeParser::SectionNode s2)
static int matchesSeparator(QStringView text, QStringView separator)
static qsizetype digitCount(QStringView str)
static int findTextEntry(QStringView text, const ShortVector< QString > &entries, QString *usedText, int *used)
#define QDTPDEBUG
static QTime actualTime(QDateTimeParser::Sections known, int hour, int hour12, int ampm, int minute, int second, int msec)
static QDate actualDate(QDateTimeParser::Sections known, QCalendar calendar, int baseYear, int year, int year2digits, int month, int day, int dayofweek)
QVarLengthArray< T, 13 > ShortVector