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

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