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 "qdatetime.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 // Arbitrarily many tokens (each up to 14 bytes) joined with / separators:
709 return std::numeric_limits<int>::max();
710
711 case CalendarPopupSection:
712 case Internal:
713 case TimeSectionMask:
714 case DateSectionMask:
715 case HourSectionMask:
716 case YearSectionMask:
717 case DayOfWeekSectionMask:
718 case DaySectionMask:
719 qWarning("QDateTimeParser::sectionMaxSize: Invalid section %s",
720 SectionNode::name(s).toLatin1().constData());
721 break;
722 }
723 return -1;
724}
725
726
727int QDateTimeParser::sectionMaxSize(int index) const
728{
729 const SectionNode &sn = sectionNode(index);
730 return sectionMaxSize(sn.type, sn.count);
731}
732
733// Separator matching
734//
735// QTBUG-114909: user may be oblivious to difference between visibly
736// indistinguishable spacing characters. For now we only treat horizontal
737// spacing characters, excluding tab, as equivalent.
738
739static int matchesSeparator(QStringView text, QStringView separator)
740{
741 const auto isSimpleSpace = [](char32_t ch) {
742 // Distinguish tab, CR and the vertical spaces from the rest:
743 return ch == u' ' || (ch > 127 && QChar::isSpace(ch));
744 };
745 // -1 if not a match, else length of prefix of text that does match.
746 // First check for exact match
747 if (!text.startsWith(separator)) {
748 // Failing that, check for space-identifying match:
749 QStringIterator given(text), sep(separator);
750 while (sep.hasNext()) {
751 if (!given.hasNext())
752 return -1;
753 char32_t s = sep.next(), g = given.next();
754 if (s != g && !(isSimpleSpace(s) && isSimpleSpace(g)))
755 return -1;
756 }
757 // One side may have used a surrogate pair space where the other didn't:
758 return given.index();
759 }
760 return separator.size();
761}
762
763/*!
764 \internal
765
766 Returns the text of section \a s. This function operates on the
767 arg text rather than edit->text().
768*/
769
770
771QString QDateTimeParser::sectionText(const QString &text, int sectionIndex, int index) const
772{
773 return text.mid(index, sectionSize(sectionIndex));
774}
775
776QString QDateTimeParser::sectionText(int sectionIndex) const
777{
778 const SectionNode &sn = sectionNode(sectionIndex);
779 return sectionText(displayText(), sectionIndex, sn.pos);
780}
781
782QDateTimeParser::ParsedSection
783QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, int offset) const
784{
785 ParsedSection result; // initially Invalid
786 const SectionNode &sn = sectionNode(sectionIndex);
787 Q_ASSERT_X(!(sn.type & Internal),
788 "QDateTimeParser::parseSection", "Internal error");
789
790 const int sectionmaxsize = sectionMaxSize(sectionIndex);
791 const bool negate = (sn.type == YearSection && m_text.size() > offset
792 && calendar.isProleptic() && m_text.at(offset) == u'-');
793 const int negativeYearOffset = negate ? 1 : 0;
794
795 QStringView sectionTextRef =
796 QStringView { m_text }.mid(offset + negativeYearOffset, sectionmaxsize);
797
798 QDTPDEBUG << "sectionValue for" << sn.name()
799 << "with text" << m_text << "and (at" << offset
800 << ") st:" << sectionTextRef;
801
802 switch (sn.type) {
803 case AmPmSection: {
804 QString sectiontext = sectionTextRef.toString();
805 int used;
806 const int ampm = findAmPm(sectiontext, sectionIndex, &used);
807 switch (ampm) {
808 case AM: // sectiontext == AM
809 case PM: // sectiontext == PM
810 result = ParsedSection(Acceptable, ampm, used);
811 break;
812 case PossibleAM: // sectiontext => AM
813 case PossiblePM: // sectiontext => PM
814 result = ParsedSection(Intermediate, ampm - 2, used);
815 break;
816 case PossibleBoth: // sectiontext => AM|PM
817 result = ParsedSection(Intermediate, 0, used);
818 break;
819 case Neither:
820 QDTPDEBUG << "invalid because findAmPm(" << sectiontext << ") returned -1";
821 break;
822 default:
823 QDTPDEBUGN("This should never happen (findAmPm returned %d)", ampm);
824 break;
825 }
826 if (result.state != Invalid)
827 m_text.replace(offset, used, sectiontext.constData(), used);
828 break; }
829 case TimeZoneSection:
830 result = findTimeZone(sectionTextRef, currentValue,
831 absoluteMax(sectionIndex),
832 absoluteMin(sectionIndex), sn.count);
833 break;
834 case MonthSection:
835 case DayOfWeekSectionShort:
836 case DayOfWeekSectionLong:
837 if (sn.count >= 3) {
838 QString sectiontext = sectionTextRef.toString();
839 int num = 0, used = 0;
840 if (sn.type == MonthSection) {
841 const QDate minDate = getMinimum(currentValue.timeRepresentation()).date();
842 const int year = currentValue.date().year(calendar);
843 const int min = (year == minDate.year(calendar)) ? minDate.month(calendar) : 1;
844 num = findMonth(sectiontext.toLower(), min, sectionIndex, year, &sectiontext, &used);
845 } else {
846 num = findDay(sectiontext.toLower(), 1, sectionIndex, &sectiontext, &used);
847 }
848
849 result = ParsedSection(Intermediate, num, used);
850 if (num != -1) {
851 m_text.replace(offset, used, sectiontext.constData(), used);
852 if (used == sectiontext.size())
853 result = ParsedSection(Acceptable, num, used);
854 }
855 break;
856 }
857 Q_FALLTHROUGH();
858 // All numeric:
859 case DaySection:
860 case YearSection:
861 case YearSection2Digits:
862 case Hour12Section:
863 case Hour24Section:
864 case MinuteSection:
865 case SecondSection:
866 case MSecSection: {
867 const auto checkSeparator = [&result, field=QStringView{m_text}.sliced(offset),
868 negativeYearOffset, sectionIndex, this]() {
869 // No-digit field if next separator is here, otherwise invalid.
870 const auto &sep = separators.at(sectionIndex + 1);
871 if (matchesSeparator(field.sliced(negativeYearOffset), sep) != -1)
872 result = ParsedSection(Intermediate, 0, negativeYearOffset);
873 else if (negativeYearOffset && matchesSeparator(field, sep) != -1)
874 result = ParsedSection(Intermediate, 0, 0);
875 else
876 return false;
877 return true;
878 };
879 int used = negativeYearOffset;
880 // We already sliced off the - sign if it was acceptable.
881 // QLocale::toUInt() would accept a sign, so we must reject it overtly:
882 if (sectionTextRef.startsWith(u'-')
883 || sectionTextRef.startsWith(u'+')) {
884 // However, a sign here may indicate a field with no digits, if it
885 // starts the next separator:
886 checkSeparator();
887 break;
888 }
889 QStringView digitsStr = sectionTextRef.left(digitCount(sectionTextRef));
890
891 if (digitsStr.isEmpty()) {
892 result = ParsedSection(Intermediate, 0, used);
893 } else {
894 const QLocale loc = locale();
895 const int absMax = absoluteMax(sectionIndex);
896 const int absMin = absoluteMin(sectionIndex);
897
898 int lastVal = -1;
899
900 for (; digitsStr.size(); digitsStr.chop(1)) {
901 bool ok = false;
902 int value = int(loc.toUInt(digitsStr, &ok));
903 if (!ok || (negate ? -value < absMin : value > absMax))
904 continue;
905
906 if (sn.type == Hour12Section) {
907 if (value > 12)
908 continue;
909 if (value == 12)
910 value = 0;
911 }
912
913 QDTPDEBUG << digitsStr << value << digitsStr.size();
914 lastVal = value;
915 used += digitsStr.size();
916 break;
917 }
918
919 if (lastVal == -1) {
920 if (!checkSeparator()) {
921 QDTPDEBUG << "invalid because" << sectionTextRef << "can't become a uint"
922 << lastVal;
923 }
924 } else {
925 if (negate)
926 lastVal = -lastVal;
927 const FieldInfo fi = fieldInfo(sectionIndex);
928 const bool unfilled = used - negativeYearOffset < sectionmaxsize;
929 if (unfilled && fi & Fraction) { // typing 2 in a zzz field should be .200, not .002
930 for (int i = used; i < sectionmaxsize; ++i)
931 lastVal *= 10;
932 }
933 // Even those *= 10s can't take last above absMax:
934 Q_ASSERT(negate ? lastVal >= absMin : lastVal <= absMax);
935 if (negate ? lastVal > absMax : lastVal < absMin) {
936 if (unfilled) {
937 result = ParsedSection(Intermediate, lastVal, used);
938 } else if (negate) {
939 QDTPDEBUG << "invalid because" << lastVal << "is greater than absoluteMax"
940 << absMax;
941 } else {
942 QDTPDEBUG << "invalid because" << lastVal << "is less than absoluteMin"
943 << absMin;
944 }
945
946 } else if (unfilled && (fi & (FixedWidth | Numeric)) == (FixedWidth | Numeric)) {
947 if (skipToNextSection(sectionIndex, currentValue, digitsStr)) {
948 const int missingZeroes = sectionmaxsize - digitsStr.size();
949 result = ParsedSection(Acceptable, lastVal, sectionmaxsize, missingZeroes);
950 m_text.insert(offset, QString(missingZeroes, u'0'));
951 ++(const_cast<QDateTimeParser*>(this)->sectionNodes[sectionIndex].zeroesAdded);
952 } else {
953 result = ParsedSection(Intermediate, lastVal, used);
954 }
955 } else if (!lastVal && !calendar.hasYearZero()
956 && (sn.type == YearSection
957 || (sn.type == YearSection2Digits && currentValue.isValid()
958 && currentValue.date().year(calendar) / 100 == 0))) {
959 // Year zero prohibited
960 result = ParsedSection(unfilled ? Acceptable : Invalid, lastVal, used);
961 } else {
962 result = ParsedSection(Acceptable, lastVal, used);
963 }
964 }
965 }
966 break; }
967 default:
968 qWarning("QDateTimeParser::parseSection Internal error (%ls %d)",
969 qUtf16Printable(sn.name()), sectionIndex);
970 return result;
971 }
972 Q_ASSERT(result.state != Invalid || result.value == -1);
973
974 return result;
975}
976
977/*!
978 \internal
979
980 Returns the day-number of a day, as close as possible to the given \a day, in
981 the specified \a month of \a year for the given \a calendar, that falls on the
982 day of the week indicated by \a weekDay.
983*/
984
985static int weekDayWithinMonth(QCalendar calendar, int year, int month, int day, int weekDay)
986{
987 // TODO: can we adapt this to cope gracefully with intercalary days (day of
988 // week > 7) without making it slower for more widely-used calendars ?
989 const int maxDay = calendar.daysInMonth(month, year); // 0 if no such month
990 day = maxDay > 1 ? qBound(1, day, maxDay) : qMax(1, day);
991 day += dayOfWeekDiff(weekDay, calendar.dayOfWeek(QDate(year, month, day, calendar)));
992 return day <= 0 ? day + 7 : maxDay > 0 && day > maxDay ? day - 7 : day;
993}
994
995/*!
996 \internal
997 Returns whichever of baseYear through baseYear + 99 has its % 100 == y2d.
998*/
999static int yearInCenturyFrom(int y2d, int baseYear)
1000{
1001 Q_ASSERT(0 <= y2d && y2d < 100);
1002 const int year = baseYear - baseYear % 100 + y2d;
1003 return year < baseYear ? year + 100 : year;
1004}
1005
1006/*!
1007 \internal
1008
1009 Returns a date consistent with the given data on parts specified by known,
1010 while staying as close to the given data as it can. Returns an invalid date
1011 when on valid date is consistent with the data.
1012*/
1013
1014static QDate actualDate(QDateTimeParser::Sections known, QCalendar calendar, int baseYear,
1015 int year, int year2digits, int month, int day, int dayofweek)
1016{
1017 QDate actual(year, month, day, calendar);
1018 if (actual.isValid() && year % 100 == year2digits && calendar.dayOfWeek(actual) == dayofweek)
1019 return actual; // The obvious candidate is fine :-)
1020
1021 if (dayofweek < 1 || dayofweek > 7) // Intercalary (or invalid): ignore
1022 known &= ~QDateTimeParser::DayOfWeekSectionMask;
1023
1024 // Assuming year > 0 ...
1025 if (year % 100 != year2digits) {
1026 if (known & QDateTimeParser::YearSection2Digits) {
1027 // Over-ride year, even if specified:
1028 year = yearInCenturyFrom(year2digits, baseYear);
1029 known &= ~QDateTimeParser::YearSection;
1030 } else {
1031 year2digits = year % 100;
1032 }
1033 }
1034 Q_ASSERT(year % 100 == year2digits);
1035
1036 if (month < 1) { // If invalid, clip to nearest valid and ignore in known.
1037 month = 1;
1038 known &= ~QDateTimeParser::MonthSection;
1039 } else if (month > 12) {
1040 month = 12;
1041 known &= ~QDateTimeParser::MonthSection;
1042 }
1043 if (!actual.isValid() && !known.testAnyFlag(QDateTimeParser::YearSectionMask)
1044 && known.testFlags(QDateTimeParser::DaySection | QDateTimeParser::MonthSection)
1045 && !calendar.isLeapYear(year) && day > calendar.daysInMonth(month, year)) {
1046 // See if a leap year works better:
1047 int leap = year + 1, stop = year + 47;
1048 // (Sweden's 1700 plan (abandoned part way through) for Julian-Gregorian
1049 // transition implied no leap year after 1697 until 1744.)
1050 while (!calendar.isLeapYear(leap) && leap < stop)
1051 ++leap;
1052 if (day <= calendar.daysInMonth(month, leap))
1053 year = leap;
1054 }
1055
1056 QDate first(year, month, 1, calendar);
1057 int last = known & QDateTimeParser::MonthSection
1058 ? (known.testAnyFlag(QDateTimeParser::YearSectionMask)
1059 ? calendar.daysInMonth(month, year) : calendar.daysInMonth(month))
1060 : 0;
1061 // We can only fix DOW if we know year as well as month (hence last):
1062 const bool fixDayOfWeek = last && known & QDateTimeParser::YearSection
1063 && known & QDateTimeParser::DayOfWeekSectionMask;
1064 // If we also know day-of-week, tweak last to the last in the month that matches it:
1065 if (fixDayOfWeek) {
1066 const int diff = (dayofweek - calendar.dayOfWeek(first) - last) % 7;
1067 Q_ASSERT(diff <= 0); // C++11 specifies (-ve) % (+ve) to be <= 0.
1068 last += diff;
1069 }
1070 if (day < 1) {
1071 if (fixDayOfWeek) {
1072 day = 1 + dayofweek - calendar.dayOfWeek(first);
1073 if (day < 1)
1074 day += 7;
1075 } else {
1076 day = 1;
1077 }
1078 known &= ~QDateTimeParser::DaySection;
1079 } else if (day > calendar.maximumDaysInMonth()) {
1080 day = last;
1081 known &= ~QDateTimeParser::DaySection;
1082 } else if (last && day > last && (known & QDateTimeParser::DaySection) == 0) {
1083 day = last;
1084 }
1085
1086 actual = QDate(year, month, day, calendar);
1087 if (!actual.isValid() // We can't do better than we have, in this case
1088 || (known & QDateTimeParser::DaySection
1089 && known & QDateTimeParser::MonthSection
1090 && known & QDateTimeParser::YearSection) // ditto
1091 || calendar.dayOfWeek(actual) == dayofweek // Good enough, use it.
1092 || (known & QDateTimeParser::DayOfWeekSectionMask) == 0) { // No contradiction, use it.
1093 return actual;
1094 }
1095
1096 /*
1097 Now it gets trickier.
1098
1099 We have some inconsistency in our data; we've been told day of week, but
1100 it doesn't fit with our year, month and day. At least one of these is
1101 unknown, though: so we can fix day of week by tweaking it.
1102 */
1103
1104 if ((known & QDateTimeParser::DaySection) == 0) {
1105 // Relatively easy to fix.
1106 day = weekDayWithinMonth(calendar, year, month, day, dayofweek);
1107 actual = QDate(year, month, day, calendar);
1108 return actual;
1109 }
1110
1111 if ((known & QDateTimeParser::MonthSection) == 0) {
1112 /*
1113 Try possible month-offsets, m, preferring small; at least one (present
1114 month doesn't work) and at most 11 (max month, 12, minus min, 1); try
1115 in both directions, ignoring any offset that takes us out of range.
1116 */
1117 for (int m = 1; m < 12; m++) {
1118 if (m < month) {
1119 actual = QDate(year, month - m, day, calendar);
1120 if (calendar.dayOfWeek(actual) == dayofweek)
1121 return actual;
1122 }
1123 if (m + month <= 12) {
1124 actual = QDate(year, month + m, day, calendar);
1125 if (calendar.dayOfWeek(actual) == dayofweek)
1126 return actual;
1127 }
1128 }
1129 // Should only get here in corner cases; e.g. day == 31
1130 actual = QDate(year, month, day, calendar); // Restore from trial values.
1131 }
1132
1133 if ((known & QDateTimeParser::YearSection) == 0) {
1134 if (known & QDateTimeParser::YearSection2Digits) {
1135 actual = calendar.matchCenturyToWeekday({year, month, day}, dayofweek);
1136 if (actual.isValid()) {
1137 Q_ASSERT(calendar.dayOfWeek(actual) == dayofweek);
1138 return actual;
1139 }
1140 } else {
1141 // Offset by 7 is usually enough, but rare cases may need more:
1142 for (int y = 1; y < 12; y++) {
1143 actual = QDate(year - y, month, day, calendar);
1144 if (calendar.dayOfWeek(actual) == dayofweek)
1145 return actual;
1146 actual = QDate(year + y, month, day, calendar);
1147 if (calendar.dayOfWeek(actual) == dayofweek)
1148 return actual;
1149 }
1150 }
1151 actual = QDate(year, month, day, calendar); // Restore from trial values.
1152 }
1153
1154 return actual; // It'll just have to do :-(
1155}
1156
1157/*!
1158 \internal
1159*/
1160
1161static QTime actualTime(QDateTimeParser::Sections known,
1162 int hour, int hour12, int ampm,
1163 int minute, int second, int msec)
1164{
1165 // If we have no conflict, or don't know enough to diagonose one, use this:
1166 QTime actual(hour, minute, second, msec);
1167 if (hour12 < 0 || hour12 > 12) { // ignore bogus value
1168 known &= ~QDateTimeParser::Hour12Section;
1169 hour12 = hour % 12;
1170 }
1171
1172 if (ampm == -1 || (known & QDateTimeParser::AmPmSection) == 0) {
1173 if ((known & QDateTimeParser::Hour12Section) == 0 || hour % 12 == hour12)
1174 return actual;
1175
1176 if ((known & QDateTimeParser::Hour24Section) == 0)
1177 hour = hour12 + (hour > 12 ? 12 : 0);
1178 } else {
1179 Q_ASSERT(ampm == 0 || ampm == 1);
1180 if (hour - hour12 == ampm * 12)
1181 return actual;
1182
1183 if ((known & QDateTimeParser::Hour24Section) == 0
1184 && known & QDateTimeParser::Hour12Section) {
1185 hour = hour12 + ampm * 12;
1186 }
1187 }
1188 actual = QTime(hour, minute, second, msec);
1189 return actual;
1190}
1191
1192/*
1193 \internal
1194*/
1195static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, const QLocale &locale)
1196{
1197 // Pick longest match that we might get.
1198 qsizetype longest = 0;
1199 // On MS-Win, at least when system zone is UTC, the tzname[]s may be empty.
1200 for (int i = 0; i < 2; ++i) {
1201 const QString zone(qTzName(i));
1202 if (zone.size() > longest && name.startsWith(zone))
1203 longest = zone.size();
1204 }
1205 // Mimic each candidate QLocale::toString() could have used, to ensure round-trips work:
1206 const auto consider = [name, &longest](QStringView zone) {
1207 if (name.startsWith(zone)) {
1208 // UTC-based zone's displayName() only includes seconds if non-zero:
1209 if (9 > longest && zone.size() == 6 && zone.startsWith("UTC"_L1)
1210 && name.sliced(6, 3) == ":00"_L1) {
1211 longest = 9;
1212 } else if (zone.size() > longest) {
1213 longest = zone.size();
1214 }
1215 }
1216 };
1217#if QT_CONFIG(timezone)
1218 /* QLocale::toString would skip this if locale == QLocale::system(), but we
1219 might not be using the same system locale as whoever generated the text
1220 we're parsing. So consider it anyway. */
1221 {
1222 const auto localWhen = QDateTime(when.date(), when.time());
1223 consider(localWhen.timeRepresentation().displayName(
1224 localWhen, QTimeZone::ShortName, locale));
1225 }
1226#else
1227 Q_UNUSED(locale);
1228#endif
1229 consider(QDateTime(when.date(), when.time()).timeZoneAbbreviation());
1230 Q_ASSERT(longest <= INT_MAX); // Timezone names are not that long.
1231 return int(longest);
1232}
1233
1234#if QT_CONFIG(timezone)
1235static auto findZoneByLongName(QStringView str, const QLocale &locale, const QDateTime &when)
1236{
1237 struct R
1238 {
1239 QTimeZone zone;
1240 qsizetype nameLength = 0;
1241 bool isValid() const { return nameLength > 0 && zone.isValid(); }
1242 } result;
1243 auto pfx = QTimeZonePrivate::findLongNamePrefix(str, locale, when.toMSecsSinceEpoch());
1244 if (!pfx) // Incomplete data in when: try without time-point.
1245 pfx = QTimeZonePrivate::findLongNamePrefix(str, locale);
1246 // (We don't want offset format to match 'tttt', so do need to limit this.)
1247 // The final fall-back for QTZL's localeName() is a zoneOffsetFormat(,,NarrowFormat,,):
1248 if (!pfx)
1249 pfx = QTimeZonePrivate::findNarrowOffsetPrefix(str, locale);
1250 if (!pfx)
1251 pfx = QTimeZonePrivate::findLongUtcPrefix(str);
1252 if (pfx) {
1253 result = R{ QTimeZone(pfx.ianaId), pfx.nameLength };
1254 Q_ASSERT(result.zone.isValid());
1255 // TODO: we should be able to take pfx.timeType into account.
1256 }
1257 return result;
1258}
1259#endif // timezone
1260
1261/*!
1262 \internal
1263*/
1264QDateTimeParser::StateNode
1265QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
1266{
1267 State state = Acceptable;
1268 bool conflicts = false;
1269 const int sectionNodesCount = sectionNodes.size();
1270 int padding = 0;
1271 int pos = 0;
1272 int year, month, day;
1273 const QDate defaultDate = defaultValue.date();
1274 const QTime defaultTime = defaultValue.time();
1275 defaultDate.getDate(&year, &month, &day);
1276 int year2digits = year % 100;
1277 int hour = defaultTime.hour();
1278 int hour12 = -1;
1279 int minute = defaultTime.minute();
1280 int second = defaultTime.second();
1281 int msec = defaultTime.msec();
1282 int dayofweek = calendar.dayOfWeek(defaultDate);
1283 QTimeZone timeZone = defaultValue.timeRepresentation();
1284
1285 int ampm = -1;
1286 Sections isSet = NoSection;
1287
1288 for (int index = 0; index < sectionNodesCount; ++index) {
1289 Q_ASSERT(state != Invalid);
1290 const QString &separator = separators.at(index);
1291 int step = matchesSeparator(QStringView{m_text}.sliced(pos), separator);
1292 if (step == -1) {
1293 QDTPDEBUG << "invalid because" << QStringView{m_text}.sliced(pos)
1294 << "does not start with" << separator
1295 << index << pos << currentSectionIndex;
1296 return StateNode();
1297 }
1298 pos += step;
1299 sectionNodes[index].pos = pos;
1300 int *current = nullptr;
1301 int zoneOffset = 0; // Needed to serve as *current when setting zone
1302 const SectionNode sn = sectionNodes.at(index);
1303 const QDateTime usedDateTime = [&] {
1304 const QDate date = actualDate(isSet, calendar, defaultCenturyStart,
1305 year, year2digits, month, day, dayofweek);
1306 const QTime time = actualTime(isSet, hour, hour12, ampm, minute, second, msec);
1307 return QDateTime(date, time, timeZone);
1308 }();
1309 ParsedSection sect = parseSection(usedDateTime, index, pos);
1310
1311 QDTPDEBUG << "sectionValue" << sn.name() << m_text
1312 << "pos" << pos << "used" << sect.used << stateName(sect.state);
1313
1314 padding += sect.zeroes;
1315 if (fixup && sect.state == Intermediate && sect.used < sn.count) {
1316 const FieldInfo fi = fieldInfo(index);
1317 if ((fi & (Numeric|FixedWidth)) == (Numeric|FixedWidth)) {
1318 const QString newText = QString::asprintf("%0*d", sn.count, sect.value);
1319 m_text.replace(pos, sect.used, newText);
1320 sect.used = sn.count;
1321 }
1322 }
1323
1324 state = qMin<State>(state, sect.state);
1325 // QDateTimeEdit can fix Intermediate and zeroes, but input needing that didn't match format:
1326 if (state == Invalid || (context == FromString && (state == Intermediate || sect.zeroes)))
1327 return StateNode();
1328
1329 switch (sn.type) {
1330 case TimeZoneSection:
1331 current = &zoneOffset;
1332 if (sect.used > 0) {
1333 // Synchronize with what findTimeZone() found:
1334 QStringView zoneName = QStringView{m_text}.sliced(pos, sect.used);
1335 Q_ASSERT(!zoneName.isEmpty()); // sect.used > 0
1336
1337 const QStringView offsetStr
1338 = zoneName.startsWith("UTC"_L1) ? zoneName.sliced(3) : zoneName;
1339 const bool isUtcOffset = offsetStr.startsWith(u'+') || offsetStr.startsWith(u'-');
1340 const bool isUtc = zoneName == "Z"_L1 || zoneName == "UTC"_L1;
1341
1342 if (isUtc || isUtcOffset) {
1343 timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
1344#if QT_CONFIG(timezone)
1345 } else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
1346 if (QTimeZone namedZone = QTimeZone(zoneName.toLatin1()); namedZone.isValid()) {
1347 timeZone = namedZone;
1348 } else {
1349 auto found = findZoneByLongName(zoneName, locale(), usedDateTime);
1350 Q_ASSERT(found.isValid());
1351 Q_ASSERT(found.nameLength == zoneName.length());
1352 timeZone = found.zone;
1353 }
1354#endif
1355 } else {
1356 timeZone = QTimeZone::LocalTime;
1357 }
1358 }
1359 break;
1360 case Hour24Section: current = &hour; break;
1361 case Hour12Section: current = &hour12; break;
1362 case MinuteSection: current = &minute; break;
1363 case SecondSection: current = &second; break;
1364 case MSecSection: current = &msec; break;
1365 case YearSection: current = &year; break;
1366 case YearSection2Digits: current = &year2digits; break;
1367 case MonthSection: current = &month; break;
1368 case DayOfWeekSectionShort:
1369 case DayOfWeekSectionLong: current = &dayofweek; break;
1370 case DaySection: current = &day; sect.value = qMax<int>(1, sect.value); break;
1371 case AmPmSection: current = &ampm; break;
1372 default:
1373 qWarning("QDateTimeParser::parse Internal error (%ls)",
1374 qUtf16Printable(sn.name()));
1375 return StateNode();
1376 }
1377 Q_ASSERT(current);
1378 Q_ASSERT(sect.state != Invalid);
1379
1380 if (sect.used > 0)
1381 pos += sect.used;
1382 QDTPDEBUG << index << sn.name() << "is set to"
1383 << pos << "state is" << stateName(state);
1384
1385 if (isSet & sn.type && *current != sect.value) {
1386 QDTPDEBUG << "CONFLICT " << sn.name() << *current << sect.value;
1387 conflicts = true;
1388 if (index != currentSectionIndex)
1389 continue;
1390 }
1391 *current = sect.value;
1392
1393 // Record the present section:
1394 isSet |= sn.type;
1395 }
1396
1397 int step = matchesSeparator(QStringView{m_text}.sliced(pos), separators.last());
1398 if (step == -1 || step + pos < m_text.size()) {
1399 QDTPDEBUG << "invalid because" << QStringView{m_text}.sliced(pos)
1400 << "does not match" << separators.last() << pos;
1401 return StateNode();
1402 }
1403
1404 if (parserType != QMetaType::QTime) {
1405 if (year % 100 != year2digits && (isSet & YearSection2Digits)) {
1406 const QDate date = actualDate(isSet, calendar, defaultCenturyStart,
1407 year, year2digits, month, day, dayofweek);
1408 if (!date.isValid()) {
1409 state = Invalid;
1410 } else if (!(isSet & YearSection)) {
1411 year = date.year(calendar);
1412 } else {
1413 conflicts = true;
1414 const SectionNode &sn = sectionNode(currentSectionIndex);
1415 if (sn.type == YearSection2Digits)
1416 year = date.year(calendar);
1417 }
1418 }
1419
1420 const auto fieldType = sectionType(currentSectionIndex);
1421 const QDate date(year, month, day, calendar);
1422 if ((!date.isValid() || dayofweek != calendar.dayOfWeek(date))
1423 && state == Acceptable && isSet & DayOfWeekSectionMask) {
1424 if (isSet & DaySection)
1425 conflicts = true;
1426 // Change to day of week should adjust day of month;
1427 // when day of month isn't set, so should change to year or month.
1428 if (currentSectionIndex == -1 || fieldType & DayOfWeekSectionMask
1429 || (!conflicts && (fieldType & (YearSectionMask | MonthSection)))) {
1430 day = weekDayWithinMonth(calendar, year, month, day, dayofweek);
1431 QDTPDEBUG << year << month << day << dayofweek
1432 << calendar.dayOfWeek(QDate(year, month, day, calendar));
1433 }
1434 }
1435
1436 bool needfixday = false;
1437 if (fieldType & DaySectionMask) {
1438 cachedDay = day;
1439 } else if (cachedDay > day && !(isSet & DayOfWeekSectionMask && state == Acceptable)) {
1440 day = cachedDay;
1441 needfixday = true;
1442 }
1443
1444 if (!calendar.isDateValid(year, month, day)) {
1445 if (day <= calendar.maximumDaysInMonth())
1446 cachedDay = day;
1447 if (day > calendar.minimumDaysInMonth() && calendar.isDateValid(year, month, 1))
1448 needfixday = true;
1449 }
1450 if (needfixday) {
1451 if (context == FromString)
1452 return StateNode();
1453 if (state == Acceptable && fixday) {
1454 day = qMin<int>(day, calendar.daysInMonth(month, year));
1455
1456 const QLocale loc = locale();
1457 for (int i=0; i<sectionNodesCount; ++i) {
1458 const SectionNode sn = sectionNode(i);
1459 if (sn.type & DaySection) {
1460 m_text.replace(sectionPos(sn), sectionSize(i), loc.toString(day));
1461 } else if (sn.type & DayOfWeekSectionMask) {
1462 const int dayOfWeek = calendar.dayOfWeek(QDate(year, month, day, calendar));
1463 const QLocale::FormatType dayFormat =
1464 (sn.type == DayOfWeekSectionShort
1465 ? QLocale::ShortFormat : QLocale::LongFormat);
1466 const QString dayName(loc.dayName(dayOfWeek, dayFormat));
1467 m_text.replace(sectionPos(sn), sectionSize(i), dayName);
1468 }
1469 }
1470 } else if (state > Intermediate) {
1471 state = Intermediate;
1472 }
1473 }
1474 }
1475
1476 if (parserType != QMetaType::QDate) {
1477 if (isSet & Hour12Section) {
1478 const bool hasHour = isSet.testAnyFlag(Hour24Section);
1479 if (ampm == -1) // If we don't know from hour, assume am:
1480 ampm = !hasHour || hour < 12 ? 0 : 1;
1481 hour12 = hour12 % 12 + ampm * 12;
1482 if (!hasHour)
1483 hour = hour12;
1484 else if (hour != hour12)
1485 conflicts = true;
1486 } else if (ampm != -1) {
1487 if (!(isSet & (Hour24Section)))
1488 hour = 12 * ampm; // Special case: only ap section
1489 else if ((ampm == 0) != (hour < 12))
1490 conflicts = true;
1491 }
1492 }
1493
1494 QDTPDEBUG << year << month << day << hour << minute << second << msec;
1495 Q_ASSERT(state != Invalid);
1496
1497 const QDate date(year, month, day, calendar);
1498 const QTime time(hour, minute, second, msec);
1499 const QDateTime when = QDateTime(date, time, timeZone);
1500
1501 if (when.time() != time || when.date() != date) {
1502 // In a spring-forward, if we hit the skipped hour, we may have been
1503 // shunted out of it.
1504
1505 // If hour wasn't specified, so we're using our default, changing it may
1506 // fix that.
1507 if (!(isSet & HourSectionMask)) {
1508 switch (parserType) {
1509 case QMetaType::QDateTime: {
1510 qint64 msecs = when.toMSecsSinceEpoch();
1511 // Fortunately, that gets a useful answer, even though when is invalid ...
1512 const QDateTime replace = QDateTime::fromMSecsSinceEpoch(msecs, timeZone);
1513 const QTime tick = replace.time();
1514 if (replace.date() == date
1515 && (!(isSet & MinuteSection) || tick.minute() == minute)
1516 && (!(isSet & SecondSection) || tick.second() == second)
1517 && (!(isSet & MSecSection) || tick.msec() == msec)) {
1518 return StateNode(replace, state, padding, conflicts);
1519 }
1520 } break;
1521 case QMetaType::QDate:
1522 // Don't care about time, so just use start of day (and ignore spec):
1523 return StateNode(date.startOfDay(QTimeZone::UTC),
1524 state, padding, conflicts);
1525 break;
1526 case QMetaType::QTime:
1527 // Don't care about date or representation, so pick a safe representation:
1528 return StateNode(QDateTime(date, time, QTimeZone::UTC),
1529 state, padding, conflicts);
1530 default:
1531 Q_UNREACHABLE_RETURN(StateNode());
1532 }
1533 } else if (state > Intermediate) {
1534 state = Intermediate;
1535 }
1536 }
1537
1538 return StateNode(when, state, padding, conflicts);
1539}
1540
1541/*!
1542 \internal
1543*/
1544
1545QDateTimeParser::StateNode
1546QDateTimeParser::parse(const QString &input, int position,
1547 const QDateTime &defaultValue, bool fixup) const
1548{
1549 const QDateTime minimum = getMinimum(defaultValue.timeRepresentation());
1550 const QDateTime maximum = getMaximum(defaultValue.timeRepresentation());
1551 m_text = input;
1552
1553 QDTPDEBUG << "parse" << input;
1554 StateNode scan = scanString(defaultValue, fixup);
1555 QDTPDEBUGN("'%s' => '%s'(%s)", m_text.toLatin1().constData(),
1556 scan.value.toString("yyyy/MM/dd hh:mm:ss.zzz"_L1).toLatin1().constData(),
1557 stateName(scan.state).toLatin1().constData());
1558
1559 if (scan.value.isValid() && scan.state != Invalid) {
1560 if (context != FromString && scan.value < minimum) {
1561 const QLatin1Char space(' ');
1562 if (scan.value >= minimum)
1563 qWarning("QDateTimeParser::parse Internal error 3 (%ls %ls)",
1564 qUtf16Printable(scan.value.toString()), qUtf16Printable(minimum.toString()));
1565
1566 bool done = false;
1567 scan.state = Invalid;
1568 const int sectionNodesCount = sectionNodes.size();
1569 for (int i=0; i<sectionNodesCount && !done; ++i) {
1570 const SectionNode &sn = sectionNodes.at(i);
1571 QString t = sectionText(m_text, i, sn.pos).toLower();
1572 if ((t.size() < sectionMaxSize(i)
1573 && ((fieldInfo(i) & (FixedWidth|Numeric)) != Numeric))
1574 || t.contains(space)) {
1575 switch (sn.type) {
1576 case AmPmSection:
1577 switch (findAmPm(t, i)) {
1578 case AM:
1579 case PM:
1580 scan.state = Acceptable;
1581 done = true;
1582 break;
1583 case Neither:
1584 scan.state = Invalid;
1585 done = true;
1586 break;
1587 case PossibleAM:
1588 case PossiblePM:
1589 case PossibleBoth: {
1590 const QDateTime copy(scan.value.addSecs(12 * 60 * 60));
1591 if (copy >= minimum && copy <= maximum) {
1592 scan.state = Intermediate;
1593 done = true;
1594 }
1595 break; }
1596 }
1597 Q_FALLTHROUGH();
1598 case MonthSection:
1599 if (sn.count >= 3) {
1600 const QDate when = scan.value.date();
1601 const int finalMonth = when.month(calendar);
1602 int tmp = finalMonth;
1603 // I know the first possible month makes the date too early
1604 while ((tmp = findMonth(t, tmp + 1, i, when.year(calendar))) != -1) {
1605 const QDateTime copy(scan.value.addMonths(tmp - finalMonth));
1606 if (copy >= minimum && copy <= maximum)
1607 break; // break out of while
1608 }
1609 if (tmp != -1) {
1610 scan.state = Intermediate;
1611 done = true;
1612 }
1613 break;
1614 }
1615 Q_FALLTHROUGH();
1616 default: {
1617 int toMin;
1618 int toMax;
1619
1620 if (sn.type & TimeSectionMask) {
1621 if (scan.value.daysTo(minimum) != 0)
1622 break;
1623
1624 const QTime time = scan.value.time();
1625 toMin = time.msecsTo(minimum.time());
1626 if (scan.value.daysTo(maximum) > 0)
1627 toMax = -1; // can't get to max
1628 else
1629 toMax = time.msecsTo(maximum.time());
1630 } else {
1631 toMin = scan.value.daysTo(minimum);
1632 toMax = scan.value.daysTo(maximum);
1633 }
1634 const int maxChange = sn.maxChange();
1635 if (toMin > maxChange) {
1636 QDTPDEBUG << "invalid because toMin > maxChange" << toMin
1637 << maxChange << t << scan.value << minimum;
1638 scan.state = Invalid;
1639 done = true;
1640 break;
1641 } else if (toMax > maxChange) {
1642 toMax = -1; // can't get to max
1643 }
1644
1645 const int min = getDigit(minimum, i);
1646 if (min == -1) {
1647 qWarning("QDateTimeParser::parse Internal error 4 (%ls)",
1648 qUtf16Printable(sn.name()));
1649 scan.state = Invalid;
1650 done = true;
1651 break;
1652 }
1653
1654 int max = toMax != -1 ? getDigit(maximum, i) : absoluteMax(i, scan.value);
1655 int pos = position + scan.padded - sn.pos;
1656 if (pos < 0 || pos >= t.size())
1657 pos = -1;
1658 if (!potentialValue(t.simplified(), min, max, i, scan.value, pos)) {
1659 QDTPDEBUG << "invalid because potentialValue(" << t.simplified() << min << max
1660 << sn.name() << "returned" << toMax << toMin << pos;
1661 scan.state = Invalid;
1662 done = true;
1663 break;
1664 }
1665 scan.state = Intermediate;
1666 done = true;
1667 break; }
1668 }
1669 }
1670 }
1671 } else {
1672 if (scan.value > maximum)
1673 scan.state = Invalid;
1674
1675 QDTPDEBUG << "not checking intermediate because scanned value is"
1676 << scan.value << minimum << maximum;
1677 }
1678 }
1679
1680 // An invalid time should only arise if we set the state to less than acceptable:
1681 Q_ASSERT(scan.value.isValid() || scan.state != Acceptable);
1682
1683 return scan;
1684}
1685
1686/*
1687 \internal
1688 \brief Returns the index in \a entries with the best prefix match to \a text
1689
1690 Scans \a entries looking for an entry overlapping \a text as much as possible
1691 (an exact match beats any prefix match; a match of the full entry as prefix of
1692 text beats any entry but one matching a longer prefix; otherwise, the match of
1693 longest prefix wins, earlier entries beating later on a draw). Records the
1694 length of overlap in *used (if \a used is non-NULL) and the first entry that
1695 overlapped this much in *usedText (if \a usedText is non-NULL).
1696 */
1697static int findTextEntry(QStringView text, const ShortVector<QString> &entries, QString *usedText, int *used)
1698{
1699 if (text.isEmpty())
1700 return -1;
1701
1702 int bestMatch = -1;
1703 int bestCount = 0;
1704 for (int n = 0; n < entries.size(); ++n)
1705 {
1706 const QString &name = entries.at(n);
1707
1708 const int limit = qMin(text.size(), name.size());
1709 int i = 0;
1710 while (i < limit && text.at(i) == name.at(i).toLower())
1711 ++i;
1712 // Full match beats an equal prefix match:
1713 if (i > bestCount || (i == bestCount && i == name.size())) {
1714 bestCount = i;
1715 bestMatch = n;
1716 if (i == name.size() && i == text.size())
1717 break; // Exact match, name == text, wins.
1718 }
1719 }
1720 if (usedText && bestMatch != -1)
1721 *usedText = entries.at(bestMatch);
1722 if (used)
1723 *used = bestCount;
1724
1725 return bestMatch;
1726}
1727
1728/*!
1729 \internal
1730 finds the first possible monthname that \a str1 can
1731 match. Starting from \a index; str should already by lowered
1732*/
1733
1734int QDateTimeParser::findMonth(QStringView str, int startMonth, int sectionIndex,
1735 int year, QString *usedMonth, int *used) const
1736{
1737 const SectionNode &sn = sectionNode(sectionIndex);
1738 if (sn.type != MonthSection) {
1739 qWarning("QDateTimeParser::findMonth Internal error");
1740 return -1;
1741 }
1742
1743 QLocale::FormatType type = sn.count == 3 ? QLocale::ShortFormat : QLocale::LongFormat;
1744 QLocale l = locale();
1745 ShortVector<QString> monthNames;
1746 monthNames.reserve(13 - startMonth);
1747 for (int month = startMonth; month <= 12; ++month)
1748 monthNames.append(calendar.monthName(l, month, year, type));
1749
1750 const int index = findTextEntry(str, monthNames, usedMonth, used);
1751 return index < 0 ? index : index + startMonth;
1752}
1753
1754int QDateTimeParser::findDay(QStringView str, int startDay, int sectionIndex, QString *usedDay, int *used) const
1755{
1756 const SectionNode &sn = sectionNode(sectionIndex);
1757 if (!(sn.type & DaySectionMask)) {
1758 qWarning("QDateTimeParser::findDay Internal error");
1759 return -1;
1760 }
1761
1762 QLocale::FormatType type = sn.count == 4 ? QLocale::LongFormat : QLocale::ShortFormat;
1763 QLocale l = locale();
1764 ShortVector<QString> daysOfWeek;
1765 daysOfWeek.reserve(8 - startDay);
1766 for (int day = startDay; day <= 7; ++day)
1767 daysOfWeek.append(l.dayName(day, type));
1768
1769 const int index = findTextEntry(str, daysOfWeek, usedDay, used);
1770 return index < 0 ? index : index + startDay;
1771}
1772
1773/*!
1774 \internal
1775
1776 Return's .value is UTC offset in seconds.
1777 The caller must verify that the offset is within a valid range.
1778 The mode is 1 for permissive parsing, 2 and 3 for strict offset-only format
1779 (no UTC prefix) with no colon for 2 and a colon for 3.
1780 */
1781QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str, int mode) const
1782{
1783 Q_ASSERT(mode > 0 && mode < 4);
1784 const bool startsWithUtc = str.startsWith("UTC"_L1);
1785 // Deal with UTC prefix if present:
1786 if (startsWithUtc) {
1787 if (mode != 1)
1788 return ParsedSection();
1789 str = str.sliced(3);
1790 if (str.isEmpty())
1791 return ParsedSection(Acceptable, 0, 3);
1792 }
1793
1794 const bool negativeSign = str.startsWith(u'-');
1795 // Must start with a sign:
1796 if (!negativeSign && !str.startsWith(u'+'))
1797 return ParsedSection();
1798 str = str.sliced(1); // drop sign
1799
1800 const int colonPosition = str.indexOf(u':');
1801 // Colon that belongs to offset is at most at position 2 (hh:mm)
1802 bool hasColon = (colonPosition >= 0 && colonPosition < 3);
1803
1804 // We deal only with digits at this point (except ':'), so collect them
1805 const int digits = hasColon ? colonPosition + 3 : 4;
1806 int i = 0;
1807 for (const int offsetLength = qMin(qsizetype(digits), str.size()); i < offsetLength; ++i) {
1808 if (i != colonPosition && !str.at(i).isDigit())
1809 break;
1810 }
1811 const int hoursLength = qMin(i, hasColon ? colonPosition : 2);
1812 if (hoursLength < 1)
1813 return ParsedSection();
1814 // Field either ends with hours or also has two digits of minutes
1815 if (i < digits) {
1816 // Only allow single-digit hours with UTC prefix or :mm suffix
1817 if (!startsWithUtc && hoursLength != 2)
1818 return ParsedSection();
1819 i = hoursLength;
1820 hasColon = false;
1821 }
1822 if (mode == (hasColon ? 2 : 3))
1823 return ParsedSection();
1824 str.truncate(i); // The rest of the string is not part of the UTC offset
1825
1826 bool isInt = false;
1827 const int hours = str.first(hoursLength).toInt(&isInt);
1828 if (!isInt)
1829 return ParsedSection();
1830 const QStringView minutesStr = str.mid(hasColon ? colonPosition + 1 : 2, 2);
1831 const int minutes = minutesStr.isEmpty() ? 0 : minutesStr.toInt(&isInt);
1832 if (!isInt)
1833 return ParsedSection();
1834
1835 // Keep in sync with QTimeZone::maxUtcOffset hours (14 at most). Also, user
1836 // could be in the middle of updating the offset (e.g. UTC+14:23) which is
1837 // an intermediate state
1838 const State status = (hours > 14 || minutes >= 60) ? Invalid
1839 : (hours == 14 && minutes > 0) ? Intermediate : Acceptable;
1840
1841 int offset = 3600 * hours + 60 * minutes;
1842 if (negativeSign)
1843 offset = -offset;
1844
1845 // Used: UTC, sign, hours, colon, minutes
1846 const int usedSymbols = (startsWithUtc ? 3 : 0) + 1 + hoursLength + (hasColon ? 1 : 0)
1847 + minutesStr.size();
1848
1849 return ParsedSection(status, offset, usedSymbols);
1850}
1851
1852/*!
1853 \internal
1854
1855 Return's .value is zone's offset, zone time - UTC time, in seconds.
1856 The caller must verify that the offset is within a valid range.
1857 See QTimeZonePrivate::isValidId() for the format of zone names.
1858 */
1859QDateTimeParser::ParsedSection
1860QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
1861{
1862 const int systemLength = startsWithLocalTimeZone(str, when, locale());
1863#if QT_CONFIG(timezone)
1864 // Collect up plausibly-valid characters; let QTimeZone work out what's
1865 // truly valid.
1866 const auto invalidZoneNameCharacter = [] (const QChar &c) {
1867 const auto cu = c.unicode();
1868 return cu >= 127u || !(memchr("+-./:_", char(cu), 6) || c.isLetterOrNumber());
1869 };
1870 int index = std::distance(str.cbegin(),
1871 std::find_if(str.cbegin(), str.cend(), invalidZoneNameCharacter));
1872
1873 // Limit name fragments (between slashes) to 20 characters.
1874 // (Valid time-zone IDs are allowed up to 14 and Android has quirks up to 17.)
1875 // Limit number of fragments to six; no known zone name has more than four.
1876 int lastSlash = -1;
1877 int count = 0;
1878 Q_ASSERT(index <= str.size());
1879 while (lastSlash < index) {
1880 int slash = str.indexOf(u'/', lastSlash + 1);
1881 if (slash < 0 || slash > index)
1882 slash = index; // i.e. the end of the candidate text
1883 else if (++count > 5)
1884 index = slash; // Truncate
1885 if (slash - lastSlash > 20)
1886 index = lastSlash + 20; // Truncate
1887 // If any of those conditions was met, index <= slash, so this exits the loop:
1888 lastSlash = slash;
1889 }
1890
1891 // Find longest IANA ID match:
1892 for (QStringView copy = str; index > systemLength; --index) {
1893 copy.truncate(index);
1894 QTimeZone zone(copy.toLatin1());
1895 if (zone.isValid())
1896 return ParsedSection(Acceptable, zone.offsetFromUtc(when), index);
1897 }
1898 // Not a known IANA ID.
1899
1900 if (auto found = findZoneByLongName(str, locale(), when); found.isValid())
1901 return ParsedSection(Acceptable, found.zone.offsetFromUtc(when), found.nameLength);
1902#endif
1903 if (systemLength > 0) // won't actually use the offset, but need it to be valid
1904 return ParsedSection(Acceptable, when.toLocalTime().offsetFromUtc(), systemLength);
1905 return ParsedSection();
1906}
1907
1908/*!
1909 \internal
1910
1911 Return's .value is zone's offset, zone time - UTC time, in seconds.
1912 See QTimeZonePrivate::isValidId() for the format of zone names.
1913
1914 The mode is the number of 't' characters in the field specifier:
1915 * 1: any recognized format
1916 * 2: only the simple offset format, without colon
1917 * 3: only the simple offset format, with colon
1918 * 4: only a zone name
1919*/
1920QDateTimeParser::ParsedSection
1921QDateTimeParser::findTimeZone(QStringView str, const QDateTime &when,
1922 int maxVal, int minVal, int mode) const
1923{
1924 Q_ASSERT(mode > 0 && mode <= 4);
1925 // Short-cut Zulu suffix when it's all there is (rather than a prefix match):
1926 if (mode == 1 && str == u'Z')
1927 return ParsedSection(Acceptable, 0, 1);
1928
1929 ParsedSection section;
1930 if (mode != 4)
1931 section = findUtcOffset(str, mode);
1932 if (mode != 2 && mode != 3 && section.used <= 0) // if nothing used, try time zone parsing
1933 section = findTimeZoneName(str, when);
1934 // It can be a well formed time zone specifier, but with value out of range
1935 if (section.state == Acceptable && (section.value < minVal || section.value > maxVal))
1936 section.state = Intermediate;
1937 if (section.used > 0)
1938 return section;
1939
1940 if (mode == 1) {
1941 // Check if string is UTC or alias to UTC, after all other options
1942 if (str.startsWith("UTC"_L1))
1943 return ParsedSection(Acceptable, 0, 3);
1944 if (str.startsWith(u'Z'))
1945 return ParsedSection(Acceptable, 0, 1);
1946 }
1947
1948 return ParsedSection();
1949}
1950
1951/*!
1952 \internal
1953
1954 Compares str to the am/pm texts returned by getAmPmText().
1955 Returns AM or PM if str is one of those texts. Failing that, it looks to see
1956 whether, ignoring spaces and case, each character of str appears in one of
1957 the am/pm texts.
1958 If neither text can be the result of the user typing more into str, returns
1959 Neither. If both texts are possible results of further typing, returns
1960 PossibleBoth. Otherwise, only one of them is a possible completion, so this
1961 returns PossibleAM or PossiblePM to indicate which.
1962
1963 \sa getAmPmText()
1964*/
1965QDateTimeParser::AmPmFinder QDateTimeParser::findAmPm(QString &str, int sectionIndex, int *used) const
1966{
1967 const SectionNode &s = sectionNode(sectionIndex);
1968 if (s.type != AmPmSection) {
1969 qWarning("QDateTimeParser::findAmPm Internal error");
1970 return Neither;
1971 }
1972 if (used)
1973 *used = str.size();
1974 if (QStringView(str).trimmed().isEmpty())
1975 return PossibleBoth;
1976
1977 const QLatin1Char space(' ');
1978 int size = sectionMaxSize(sectionIndex);
1979
1980 enum {
1981 amindex = 0,
1982 pmindex = 1
1983 };
1984 QString ampm[2];
1985 ampm[amindex] = getAmPmText(AmText, Case(s.count));
1986 ampm[pmindex] = getAmPmText(PmText, Case(s.count));
1987 for (int i = 0; i < 2; ++i)
1988 ampm[i].truncate(size);
1989
1990 QDTPDEBUG << "findAmPm" << str << ampm[0] << ampm[1];
1991
1992 if (str.startsWith(ampm[amindex], Qt::CaseInsensitive)) {
1993 str = ampm[amindex];
1994 return AM;
1995 } else if (str.startsWith(ampm[pmindex], Qt::CaseInsensitive)) {
1996 str = ampm[pmindex];
1997 return PM;
1998 } else if (context == FromString || (str.count(space) == 0 && str.size() >= size)) {
1999 return Neither;
2000 }
2001 size = qMin(size, str.size());
2002
2003 bool broken[2] = {false, false};
2004 for (int i=0; i<size; ++i) {
2005 const QChar ch = str.at(i);
2006 if (ch != space) {
2007 for (int j=0; j<2; ++j) {
2008 if (!broken[j]) {
2009 int index = ampm[j].indexOf(ch);
2010 QDTPDEBUG << "looking for" << ch
2011 << "in" << ampm[j] << "and got" << index;
2012 if (index == -1) {
2013 if (ch.category() == QChar::Letter_Uppercase) {
2014 index = ampm[j].indexOf(ch.toLower());
2015 QDTPDEBUG << "trying with" << ch.toLower()
2016 << "in" << ampm[j] << "and got" << index;
2017 } else if (ch.category() == QChar::Letter_Lowercase) {
2018 index = ampm[j].indexOf(ch.toUpper());
2019 QDTPDEBUG << "trying with" << ch.toUpper()
2020 << "in" << ampm[j] << "and got" << index;
2021 }
2022 if (index == -1) {
2023 broken[j] = true;
2024 if (broken[amindex] && broken[pmindex]) {
2025 QDTPDEBUG << str << "didn't make it";
2026 return Neither;
2027 }
2028 continue;
2029 } else {
2030 str[i] = ampm[j].at(index); // fix case
2031 }
2032 }
2033 ampm[j].remove(index, 1);
2034 }
2035 }
2036 }
2037 }
2038 if (!broken[pmindex] && !broken[amindex])
2039 return PossibleBoth;
2040 return (!broken[amindex] ? PossibleAM : PossiblePM);
2041}
2042
2043/*!
2044 \internal
2045 Max number of units that can be changed by this section.
2046*/
2047
2048int QDateTimeParser::SectionNode::maxChange() const
2049{
2050 switch (type) {
2051 // Time. unit is msec
2052 case MSecSection: return 999;
2053 case SecondSection: return 59 * 1000;
2054 case MinuteSection: return 59 * 60 * 1000;
2055 case Hour24Section: case Hour12Section: return 59 * 60 * 60 * 1000;
2056
2057 // Date. unit is day
2058 case DayOfWeekSectionShort:
2059 case DayOfWeekSectionLong: return 7;
2060 case DaySection: return 30;
2061 case MonthSection: return 365 - 31;
2062 case YearSection: return 9999 * 365;
2063 case YearSection2Digits: return 100 * 365;
2064 default:
2065 qWarning("QDateTimeParser::maxChange() Internal error (%ls)",
2066 qUtf16Printable(name()));
2067 }
2068
2069 return -1;
2070}
2071
2072QDateTimeParser::FieldInfo QDateTimeParser::fieldInfo(int index) const
2073{
2074 FieldInfo ret;
2075 const SectionNode &sn = sectionNode(index);
2076 switch (sn.type) {
2077 case MSecSection:
2078 ret |= Fraction;
2079 Q_FALLTHROUGH();
2080 case SecondSection:
2081 case MinuteSection:
2082 case Hour24Section:
2083 case Hour12Section:
2084 case YearSection2Digits:
2085 ret |= AllowPartial;
2086 Q_FALLTHROUGH();
2087 case YearSection:
2088 ret |= Numeric;
2089 if (sn.count != 1)
2090 ret |= FixedWidth;
2091 break;
2092 case MonthSection:
2093 case DaySection:
2094 switch (sn.count) {
2095 case 2:
2096 ret |= FixedWidth;
2097 Q_FALLTHROUGH();
2098 case 1:
2099 ret |= (Numeric|AllowPartial);
2100 break;
2101 }
2102 break;
2103 case DayOfWeekSectionShort:
2104 case DayOfWeekSectionLong:
2105 if (sn.count == 3)
2106 ret |= FixedWidth;
2107 break;
2108 case AmPmSection:
2109 // Some locales have different length AM and PM texts.
2110 if (getAmPmText(AmText, Case(sn.count)).size()
2111 == getAmPmText(PmText, Case(sn.count)).size()) {
2112 // Only relevant to DateTimeEdit's fixups in parse().
2113 ret |= FixedWidth;
2114 }
2115 break;
2116 case TimeZoneSection:
2117 break;
2118 default:
2119 qWarning("QDateTimeParser::fieldInfo Internal error 2 (%d %ls %d)",
2120 index, qUtf16Printable(sn.name()), sn.count);
2121 break;
2122 }
2123 return ret;
2124}
2125
2126QString QDateTimeParser::SectionNode::format() const
2127{
2128 QChar fillChar;
2129 switch (type) {
2130 case AmPmSection: return count == 1 ? "ap"_L1 : count == 2 ? "AP"_L1 : "Ap"_L1;
2131 case MSecSection: fillChar = u'z'; break;
2132 case SecondSection: fillChar = u's'; break;
2133 case MinuteSection: fillChar = u'm'; break;
2134 case Hour24Section: fillChar = u'H'; break;
2135 case Hour12Section: fillChar = u'h'; break;
2136 case DayOfWeekSectionShort:
2137 case DayOfWeekSectionLong:
2138 case DaySection: fillChar = u'd'; break;
2139 case MonthSection: fillChar = u'M'; break;
2140 case YearSection2Digits:
2141 case YearSection: fillChar = u'y'; break;
2142 default:
2143 qWarning("QDateTimeParser::sectionFormat Internal error (%ls)",
2144 qUtf16Printable(name(type)));
2145 return QString();
2146 }
2147 if (fillChar.isNull()) {
2148 qWarning("QDateTimeParser::sectionFormat Internal error 2");
2149 return QString();
2150 }
2151 return QString(count, fillChar);
2152}
2153
2154
2155/*!
2156 \internal
2157
2158 Returns \c true if str can be modified to represent a
2159 number that is within min and max.
2160*/
2161
2162bool QDateTimeParser::potentialValue(QStringView str, int min, int max, int index,
2163 const QDateTime &currentValue, int insert) const
2164{
2165 if (str.isEmpty())
2166 return true;
2167
2168 const int size = sectionMaxSize(index);
2169 int val = (int)locale().toUInt(str);
2170 const SectionNode &sn = sectionNode(index);
2171 if (sn.type == YearSection2Digits) {
2172 const int year = currentValue.date().year(calendar);
2173 val += year - (year % 100);
2174 }
2175 if (val >= min && val <= max && str.size() == size)
2176 return true;
2177 if (val > max || (str.size() == size && val < min))
2178 return false;
2179
2180 const int len = size - str.size();
2181 for (int i=0; i<len; ++i) {
2182 for (int j=0; j<10; ++j) {
2183 if (potentialValue(str + QLatin1Char('0' + j), min, max, index, currentValue, insert)) {
2184 return true;
2185 } else if (insert >= 0) {
2186 const QString tmp = str.left(insert) + QLatin1Char('0' + j) + str.mid(insert);
2187 if (potentialValue(tmp, min, max, index, currentValue, insert))
2188 return true;
2189 }
2190 }
2191 }
2192
2193 return false;
2194}
2195
2196/*!
2197 \internal
2198*/
2199bool QDateTimeParser::skipToNextSection(int index, const QDateTime &current, QStringView text) const
2200{
2201 Q_ASSERT(text.size() < sectionMaxSize(index));
2202 const SectionNode &node = sectionNode(index);
2203 int min = absoluteMin(index);
2204 int max = absoluteMax(index, current);
2205 // Time-zone field is only numeric if given as offset from UTC:
2206 if (node.type != TimeZoneSection || current.timeSpec() == Qt::OffsetFromUTC) {
2207 const QDateTime maximum = getMaximum(current.timeRepresentation());
2208 const QDateTime minimum = getMinimum(current.timeRepresentation());
2209 // Range from minimum to maximum might not contain current if an earlier
2210 // field's value was full-width but out of range. In such a case the
2211 // parse is already headed for Invalid, so it doesn't matter that we get
2212 // the wrong range of values for the current field here.
2213
2214 QDateTime tmp = current;
2215 if (!setDigit(tmp, index, min) || tmp < minimum)
2216 min = getDigit(minimum, index);
2217
2218 if (!setDigit(tmp, index, max) || tmp > maximum)
2219 max = getDigit(maximum, index);
2220 }
2221 int pos = cursorPosition() - node.pos;
2222 if (pos < 0 || pos >= text.size())
2223 pos = -1;
2224
2225 /*
2226 If the value potentially can become another valid entry we don't want to
2227 skip to the next. E.g. In a M field (month without leading 0) if you type
2228 1 we don't want to autoskip (there might be [012] following) but if you
2229 type 3 we do.
2230 */
2231 return !potentialValue(text, min, max, index, current, pos);
2232}
2233
2234/*!
2235 \internal
2236 For debugging. Returns the name of the section \a s.
2237*/
2238
2239QString QDateTimeParser::SectionNode::name(QDateTimeParser::Section s)
2240{
2241 switch (s) {
2242 case AmPmSection: return "AmPmSection"_L1;
2243 case DaySection: return "DaySection"_L1;
2244 case DayOfWeekSectionShort: return "DayOfWeekSectionShort"_L1;
2245 case DayOfWeekSectionLong: return "DayOfWeekSectionLong"_L1;
2246 case Hour24Section: return "Hour24Section"_L1;
2247 case Hour12Section: return "Hour12Section"_L1;
2248 case MSecSection: return "MSecSection"_L1;
2249 case MinuteSection: return "MinuteSection"_L1;
2250 case MonthSection: return "MonthSection"_L1;
2251 case SecondSection: return "SecondSection"_L1;
2252 case TimeZoneSection: return "TimeZoneSection"_L1;
2253 case YearSection: return "YearSection"_L1;
2254 case YearSection2Digits: return "YearSection2Digits"_L1;
2255 case NoSection: return "NoSection"_L1;
2256 case FirstSection: return "FirstSection"_L1;
2257 case LastSection: return "LastSection"_L1;
2258 default: return "Unknown section "_L1 + QString::number(int(s));
2259 }
2260}
2261
2262/*!
2263 \internal
2264 For debugging. Returns the name of the state \a s.
2265*/
2266
2267QString QDateTimeParser::stateName(State s) const
2268{
2269 switch (s) {
2270 case Invalid: return "Invalid"_L1;
2271 case Intermediate: return "Intermediate"_L1;
2272 case Acceptable: return "Acceptable"_L1;
2273 default: return "Unknown state "_L1 + QString::number(s);
2274 }
2275}
2276
2277
2278/*!
2279 \internal
2280 Compute a defaultValue to pass to parse().
2281*/
2282QDateTime QDateTimeParser::baseDate(const QTimeZone &zone) const
2283{
2284 QDateTime when = QDate(defaultCenturyStart, 1, 1).startOfDay(zone);
2285 if (const QDateTime start = getMinimum(zone); when < start)
2286 return start;
2287 if (const QDateTime end = getMaximum(zone); when > end)
2288 return end;
2289 return when;
2290}
2291
2292// Only called when we want only one of date or time; use UTC to avoid bogus DST issues.
2293bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time, int baseYear) const
2294{
2295 defaultCenturyStart = baseYear;
2296 const StateNode tmp = parse(t, -1, baseDate(QTimeZone::UTC), false);
2297 if (tmp.state != Acceptable || tmp.conflicts)
2298 return false;
2299
2300 if (time) {
2301 Q_ASSERT(!date);
2302 const QTime t = tmp.value.time();
2303 if (!t.isValid())
2304 return false;
2305 *time = t;
2306 }
2307
2308 if (date) {
2309 Q_ASSERT(!time);
2310 const QDate d = tmp.value.date();
2311 if (!d.isValid())
2312 return false;
2313 *date = d;
2314 }
2315 return true;
2316}
2317
2318// Only called when we want both date and time; default to local time.
2319bool QDateTimeParser::fromString(const QString &t, QDateTime *datetime, int baseYear) const
2320{
2321 defaultCenturyStart = baseYear;
2322 const StateNode tmp = parse(t, -1, baseDate(QTimeZone::LocalTime), false);
2323 if (datetime)
2324 *datetime = tmp.value;
2325 return tmp.state >= Intermediate && !tmp.conflicts && tmp.value.isValid();
2326}
2327
2328QDateTime QDateTimeParser::getMinimum(const QTimeZone &zone) const
2329{
2330 // NB: QDateTimeParser always uses Qt::LocalTime time spec by default. If
2331 // any subclass needs a changing time spec, it must override this
2332 // method. At the time of writing, this is done by QDateTimeEditPrivate.
2333
2334 // Cache the only case (and make sure it knows its UTC offset):
2335 static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN.startOfDay());
2336 static const QDateTime utcTimeMin = localTimeMin.toUTC();
2337 switch (zone.timeSpec()) {
2338 case Qt::LocalTime:
2339 return localTimeMin;
2340 case Qt::UTC:
2341 return utcTimeMin;
2342 case Qt::OffsetFromUTC:
2343 case Qt::TimeZone:
2344 break;
2345 }
2346 return utcTimeMin.toTimeZone(zone);
2347}
2348
2349QDateTime QDateTimeParser::getMaximum(const QTimeZone &zone) const
2350{
2351 // NB: QDateTimeParser always uses Qt::LocalTime time spec by default. If
2352 // any subclass needs a changing time spec, it must override this
2353 // method. At the time of writing, this is done by QDateTimeEditPrivate.
2354
2355 // Cache the only case
2356 static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay());
2357 static const QDateTime utcTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay(QTimeZone::UTC));
2358 switch (zone.timeSpec()) {
2359 case Qt::LocalTime:
2360 return localTimeMax;
2361 case Qt::UTC:
2362 return utcTimeMax;
2363 case Qt::OffsetFromUTC:
2364 case Qt::TimeZone:
2365 break;
2366 }
2367 return utcTimeMax.toTimeZone(zone);
2368}
2369
2370QString QDateTimeParser::getAmPmText(AmPm ap, Case cs) const
2371{
2372 const QLocale loc = locale();
2373 QString raw = ap == AmText ? loc.amText() : loc.pmText();
2374 switch (cs)
2375 {
2376 case UpperCase: return std::move(raw).toUpper();
2377 case LowerCase: return std::move(raw).toLower();
2378 case NativeCase: return raw;
2379 }
2380 Q_UNREACHABLE_RETURN(raw);
2381}
2382
2383/*
2384 \internal
2385*/
2386
2387bool operator==(QDateTimeParser::SectionNode s1, QDateTimeParser::SectionNode s2)
2388{
2389 return (s1.type == s2.type) && (s1.pos == s2.pos) && (s1.count == s2.count);
2390}
2391
2392/*!
2393 \internal
2394 Sets \a cal as the calendar to use. The default is Gregorian.
2395*/
2396
2397void QDateTimeParser::setCalendar(QCalendar cal)
2398{
2399 calendar = cal;
2400}
2401
2402QT_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