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