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