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