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

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