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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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