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

source code of qtbase/src/corelib/time/qdatetimeparser.cpp