Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtparsetemporal.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 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#include "private/qtparsetemporal_p.h"
5
6#include "private/qcalendarmath_p.h"
7#include "private/qlocale_p.h"
8#include "private/qstringiterator_p.h"
9#include "private/qttemporalpattern_p.h"
10
11#include <algorithm> // sort, stable_sort
12#include <QtCore/qxpfunctional.h>
13#include <optional>
14#include <utility> // exchange, move, pair
15#include <vector>
16
17QT_BEGIN_NAMESPACE
18
19namespace {
20using namespace QtParseTemporal;
21using namespace QtTemporalPattern;
22
24{
26 int periodInDay = -1; // 0: am, 1: pm
27 int hourMod12 = 0; // 1 through 12
28 int yearWithinCentury = -1; // 0 through 99
29 static constexpr int UnknownAmHour = -12, UnknownPmHour = 36;
30 enum Flaw : quint16 {
31 // Flaws that justify prefering a shorter parse without the flaw over
32 // longer with it, in order of decreasing severity:
33 Irreconcilable = 1, // field values cannot be reconciled
34 // Ideally continuations() would catch irreconcilable issues, but if one
35 // is expensive to spot it can be left for resolve() to flag up.
36 LegacyResolves = 2, // field values can only be resolved by ignoring timeType
37 ResolutionChanges = 4, // resolved values don't match parsed values
38 ZeroPad = 8, // used zero-padded part of text where field didn't require it
39 Narrow = 0x10, // used fewer digits from text than field width, where allowed
40
41 // Flaws not worth giving up a longer parse over, in decreasing order of
42 // strength of preference among those of equal length:
43 SelfResolved = 0x100, // ambiguous field values resolve cleanly
44 };
45 Q_DECLARE_FLAGS(Flaws, Flaw)
47
48 // Constructor for initial empty parse:
49 PartialParse(qsizetype from) { results.startIndex = results.endIndex = from; }
50 // Constructors extending a parse with something more:
51 PartialParse(const PartialParse &base, const QtParseCommon::ParsedText &more)
52 : PartialParse(base)
53 {
54 Q_ASSERT(results.endIndex == more.startIndex);
55 results.endIndex = more.endIndex;
56 }
57 PartialParse(const PartialParse &base, const QtParseTimeZone::ParsedZone &more)
59 {
60 results.zone = more.zone;
61 results.timeType = more.timeType;
62 }
63
64 Qt::weak_ordering compare(const PartialParse &alt) const noexcept
65 {
66 // Measures of how this->wanton differs from alt.wanton: it's worse if
67 // it has a flaw that alt lacks, better if the opposite. Here, "less" is
68 // used to mean "this is better than alt" as we sort better entries
69 // earlier in our lists of partial parse candidates.
70 const auto order = [better = alt.wanton & ~wanton,
71 worse = wanton & ~alt.wanton](Flaw test) {
72 Q_ASSERT(!(worse & better)); // So at most one of these testFlag()s is true:
73 if (better.testFlag(test))
74 return Qt::weak_ordering::less;
75 if (worse.testFlag(test))
76 return Qt::weak_ordering::greater;
77 return Qt::weak_ordering::equivalent;
78 };
79
80 if (auto res = order(Flaw::Irreconcilable); res != 0)
81 return res;
82
83 // In decreasing order of severity
84 if (auto res = order(Flaw::LegacyResolves); res != 0)
85 return res;
86 if (auto res = order(Flaw::ResolutionChanges); res != 0)
87 return res;
88 if (auto res = order(Flaw::Narrow); res != 0)
89 return res;
90
91 // The above take precedence over size: a shorter parse without those
92 // flaws is better than a longer one with them.
93
94 // Longer is better, to be understood as "sorts before" i.e. less than.
95 if (auto res = Qt::compareThreeWay(alt.results.size(), results.size()); res != 0)
96 return res;
97
98 // The following remains as a preference only among matches of the same
99 // length: it's nice to avoid, but a longer parse is still better.
100
101 if (auto res = order(Flaw::ZeroPad); res != 0)
102 return res;
103 if (auto res = order(Flaw::SelfResolved); res != 0)
104 return res;
105
106 return Qt::weak_ordering::equivalent;
107 }
108};
109Q_DECLARE_OPERATORS_FOR_FLAGS(PartialParse::Flaws)
110
111QLocaleData::DigitSequence
112parseDigitSequence(QStringView text, qsizetype from, const QLocale &locale, bool allowSign)
113{
114 const auto *const data = QLocalePrivate::get(locale)->m_data;
115 using DS = QLocaleData::DigitSequence;
116 DS::Options flags;
117 if (allowSign)
118 flags.setFlag(DS::Option::AllowSign, true);
119 return data->digitSequence(text, flags, from);
120}
121
122std::vector<PartialParse> spacePadExtend(std::vector<PartialParse> matched, QStringView text)
123{
124 // Pass the whole text: the results.endIndex of the last entry in matched is
125 // an index into it that we shall use to add entries that extend that entry.
126 Q_ASSERT(!matched.empty());
127 // Assumes matched.back()'s last field is allowed to end in space-padding
128 // and inserts a partial parse resulting from accepting each subsequent
129 // space as extending the match. Each longer match is inserted before all
130 // shorter matches. Only extensions of matched.back() are added, so call
131 // after adding each entry to matched, if adding several.
132 const qsizetype position = matched.size() - 1;
133 PartialParse copy = matched.back();
134 QStringIterator iter(text, copy.results.endIndex);
135 while (iter.hasNext() && QChar::isSpace(iter.next())) {
136 Q_ASSERT(iter.index() > copy.results.endIndex);
137 copy.results.endIndex = iter.index();
138 matched.insert(matched.begin() + position, copy);
139 }
140 return matched;
141}
142
143QtParseCommon::ParsedText matchesAt(QStringView text, qsizetype from, const QString &sought,
144 TemporalFieldFlags flags)
145{
146 using F = TemporalFieldFlag;
147 const bool allowLeadingSpace = flags.testFlag(F::SpacePad);
148 Q_ASSERT(sought.size() > 0);
149 // Note: returns the first match within text[from:]. If sought is all space
150 // and SpacePad is set, there may be later matches if text[from:] starts
151 // with more space than (possibly some non-matching spaces, then) that.
152 // caller is expected to follow the match implied by the return from this
153 // with more generated by spacePadExtend().
154
155 const auto beginLength = [flex = flags.testFlag(F::FlexSpace)]
156 (QStringView view, QStringView target, Qt::CaseSensitivity cs = Qt::CaseSensitive) {
157 // Technical hitch: case-insensitive comparison may match a string of
158 // different length. Roll a brute-force length-determining version:
159 const auto matchFront = [cs](QStringView view, QStringView target) {
160 if (view.startsWith(target, cs)) {
161 qsizetype length = target.size();
162 while (view.first(length - 1).startsWith(target, cs))
163 --length;
164 while (!view.first(length).startsWith(target, cs))
165 ++length;
166 Q_ASSERT(length > 0);
167 return length;
168 }
169 return qsizetype(-1);
170 };
171 const auto spaceForward = [](QStringIterator &iter) {
172 // Steps iter past next non-space, returns index at which it appeared.
173 qsizetype used;
174 do {
175 used = iter.index();
176 } while (iter.hasNext() && QChar::isSpace(iter.next()));
177 return used;
178 };
179 constexpr qsizetype failed = 0;
180 qsizetype matched = 0;
181 if (flex) {
182 QStringIterator iter(target);
183 while (iter.hasNext()) {
184 qsizetype head = iter.index();
185 if (QChar::isSpace(iter.next())) {
186 qsizetype same = head > 0 ? matchFront(view, target.first(head)) : 0;
187 if (same < 0)
188 return failed;
189 QStringIterator viter(view, same);
190 // Require at least one spacing character in view to match those in target:
191 if (!viter.hasNext() || !QChar::isSpace(viter.next()))
192 return failed;
193 same = spaceForward(viter);
194 matched += same;
195 view = view.sliced(same);
196 target = target.sliced(spaceForward(iter));
197 iter = QStringIterator(target);
198 }
199 }
200 }
201 const qsizetype tail = target.isEmpty() ? 0 : matchFront(view, target);
202 if (tail < 0)
203 return failed;
204 return matched + tail;
205 };
206 // TODO: consider a comparison that ignores Unicode invisibles, like BiDi
207 // markers, when matching.
208 qsizetype offset = 0;
209 do {
210 QStringView view = text.sliced(from + offset);
211 if (flags.testFlag(F::IgnoreCase)) {
212 if (qsizetype match = beginLength(view, sought, Qt::CaseInsensitive))
213 return {from, from + offset + match};
214 } else if (flags.testAnyFlags(F::LowerCase | F::UpperCase)) {
215 // If either case is specified, only match specified cases.
216 // If both cases are specified, accept either (but not mixed).
217 if (flags.testFlag(F::LowerCase)) {
218 if (qsizetype match = beginLength(view, sought.toLower()))
219 return {from, from + offset + match};
220 }
221 if (flags.testFlag(F::UpperCase)) {
222 if (qsizetype match = beginLength(view, sought.toUpper()))
223 return {from, from + offset + match};
224 }
225 // Otherwise, only an exact match is accepted:
226 } else if (qsizetype match = beginLength(view, sought)) {
227 return {from, from + offset + match};
228 }
229
230 // No match at this position; maybe later if leading space is allowed:
231 if (!allowLeadingSpace) {
232 Q_ASSERT(!offset);
233 break;
234 }
235
236 // Consume one space at a time until we find a match:
237 QStringIterator iter(text.sliced(from), offset);
238 if (!iter.hasNext() || !QChar::isSpace(iter.next()))
239 break;
240
241 Q_ASSERT(iter.index() > offset);
242 offset = iter.index();
243 } while (text.size() >= offset + sought.size() / 2);
244 // Loop wants to test text.size() >= offset + sought.size(), but see beginLength().
245 return {};
246}
247
248bool longerEarlier(const PartialParse &left, const PartialParse &right)
249{
250 // True if we want left before right in our sorted lists.
251 // We want longer matches before shorter:
252 return left.results.endIndex > right.results.endIndex;
253}
254
255void forEachLocaleFormat(TemporalFieldFlags flags,
256 qxp::function_ref<void(QLocale::FormatType) const> action)
257{
258 using Flag = TemporalFieldFlag;
259 constexpr auto Widths = FieldGroup::WidthMask;
260 if (matchesFlagWithin(flags, Flag::Wide, Widths))
261 action(QLocale::LongFormat);
262 if (matchesFlagsWithin(flags, Flag::Short | Flag::Abbreviated, Widths))
263 action(QLocale::ShortFormat);
264 if (matchesFlagWithin(flags, Flag::Narrow, Widths))
265 action(QLocale::NarrowFormat);
266}
267
269{
270 const QLocale locale;
271 const QCalendar calendar;
272 const std::optional<int> baseYear;
273
274 // Numeric
275 struct FieldConfig
276 {
277 // Where to write the int, once read:
278 int &(*target)(PartialParse &);
279 // Acceptable values:
280 int maxValue = 0; // 0 means unbounded
281 int unset; // Default value in ParsedTemporal, invalid for field.
282 // Form of the parsed text:
283 qsizetype width; // min digits
284 qsizetype maxDigits = 0; // <= 0 means unbounded
285 // If unbounded, beyond max(width, roundAfter, -maxDigits) prefer fewer digits to more.
286 qsizetype roundAfter = -1; // >= 0: is fractional part: round to this many digits
287 bool allowSign = false;
288 };
289 // For use as FieldConfig::target:
290 static int &millisTarget(PartialParse &grow) { return grow.results.millis; }
291 static int &secondTarget(PartialParse &grow) { return grow.results.second; }
292 static int &minuteTarget(PartialParse &grow) { return grow.results.minute; }
293 static int &hourTarget(PartialParse &grow) { return grow.results.hour; }
294 static int &hourMod12Target(PartialParse &grow) { return grow.hourMod12; }
295 static int &dayOfWeekTarget(PartialParse &grow) { return grow.results.dayOfWeek; }
296 static int &dayOfMonthTarget(PartialParse &grow) { return grow.results.dayOfMonth; }
297 static int &monthTarget(PartialParse &grow) { return grow.results.month; }
298 static int &yearTarget(PartialParse &grow)
299 {
300 if (!grow.results.year)
301 grow.results.year = 0;
302 return *grow.results.year;
303 }
304 static int &yearWithinCenturyTarget(PartialParse &grow) { return grow.yearWithinCentury; }
305
306 std::vector<PartialParse>
307 numericExtend(const PartialParse &base, QStringView text,
308 TemporalFieldFlags flags, FieldConfig &&config) const;
309
310 // Verbal, Standalone:
311 std::vector<PartialParse> monthNameExtend(const PartialParse &base, QStringView text,
312 TemporalFieldFlags flags) const;
313 std::vector<PartialParse> dayNameExtend(const PartialParse &base, QStringView text,
314 TemporalFieldFlags flags) const;
315 std::pair<qsizetype, int> dayPeriodPrefix(const PartialParse &base, QStringView text,
316 TemporalFieldFlags flags) const;
317public:
318 TemporalFieldMatcher(const QLocale &loc, QCalendar cal, std::optional<int> centuryStart)
320 {}
321
322 std::vector<PartialParse> continuations(const PartialParse &base, QStringView text,
323 const TemporalField &field) const;
324 bool isSelfConsistent(const PartialParse &parsed, TemporalFieldCategory category) const;
325 bool resolve(PartialParse &parsed) const;
326};
327
329 TemporalFieldCategory category) const
330{
331 // Take into account calendar, and potentially baseYear, but only do cheap
332 // checks. This will be run on *each* candidate parse after *each* field,
333 // need not check conditions the current field could not have affected.
334 using Cat = TemporalFieldCategory;
335 if (category == Cat::Literal) // Can't have introduced any inconsistency.
336 return true;
337
338 const bool newYear = category == Cat::Year || category == Cat::YearWithinCentury;
339 if (newYear && parse.yearWithinCentury >= 0 && parse.results.year
340 && (*parse.results.year - parse.yearWithinCentury) % 100) {
341 return false;
342 }
343
344 const bool newDate = (newYear || category == Cat::Month || category == Cat::DayOfMonth
345 || category == Cat::DayOfWeek);
346 if (newDate && parse.results.month && parse.results.dayOfMonth) {
347 // Calendrical calculations: somewhat expensive, but still arithmetic.
348 if (parse.results.year) {
349 if (!calendar.isDateValid(*parse.results.year, parse.results.month,
350 parse.results.dayOfMonth)) {
351 return false;
352 }
353 if (parse.results.dayOfWeek) {
354 QDate date = calendar.dateFromParts(*parse.results.year, parse.results.month,
355 parse.results.dayOfMonth);
356 if (calendar.dayOfWeek(date) != parse.results.dayOfWeek)
357 return false;
358 }
359 } else if (calendar.daysInMonth(parse.results.month) < parse.results.dayOfMonth) {
360 return false;
361 }
362 }
363
364 if ((category == Cat::PeriodInDay && parse.results.hour >= 0)
365 || (category == Cat::Hour && parse.periodInDay >= 0)) {
366 // 00, 01, ... 11 are 12, 1, ... 11 am; 12, 13, ... 23 are 12, 1, ..., 11 pm.
367 if (parse.periodInDay ? parse.results.hour < 12 : parse.results.hour >= 12)
368 return false;
369 }
370
371 if ((category == Cat::Hour && parse.hourMod12 > 0)
372 || (category == Cat::HourMod12 && parse.results.hour >= 0)) {
373 if ((parse.results.hour - parse.hourMod12) % 12)
374 return false;
375 }
376 return true;
377}
378
380{
381 // Final pass, modifying parsed as needed, true if parse.result has been
382 // given a value consistent with all fields of parse. Applies fully rigorous
383 // checks, given what isSelfConsistent() already checked. May record flaws
384 // in parse.wanton where relevant tests reveal them.
385 if (parse.yearWithinCentury >= 0) {
386 if (parse.results.year) {
387 // Previously checked by isSelfConsistent():
388 Q_ASSERT((*parse.results.year - parse.yearWithinCentury) % 100 == 0);
389 } else if (baseYear) {
390 const auto baseSplit =QRoundingDown::qDivMod<100>(*baseYear);
391 int year = baseSplit.quotient * 100 + parse.yearWithinCentury;
392 if (parse.yearWithinCentury < baseSplit.remainder)
393 year += 100;
394
395 if (parse.results.month) {
396 // Check the year has this month and, if given, enough days in
397 // it for dayOfMonth:
398 const auto enough = [dom = parse.results.dayOfMonth](int dim) {
399 return dim > 0 && (!dom || dom <= dim);
400 };
401 if (!enough(calendar.daysInMonth(parse.results.month, year))) {
402 // Search outwards for a better century:
403 bool fixed = false;
404 for (int off = 1; off < 10; ++off) {
405 int offset = off * 100;
406 if (enough(calendar.daysInMonth(parse.results.month, year + offset))) {
407 year += offset;
408 fixed = true;
409 break;
410 }
411 if (enough(calendar.daysInMonth(parse.results.month, year - offset))) {
412 year -= offset;
413 fixed = true;
414 break;
415 }
416 }
417 // No century within a millennium each way will do:
418 if (!fixed)
419 return false;
420 }
421
422 if (parse.results.dayOfMonth) {
423 if (parse.results.dayOfWeek) {
424 QCalendar::YearMonthDay ymd
425 = { year, parse.results.month, parse.results.dayOfMonth };
426 const QDate resolved
427 = calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
428 if (!resolved.isValid())
429 return false;
430 year = resolved.year(calendar);
431 } else {
432 const QDate resolved(year, parse.results.month, parse.results.dayOfMonth);
433 if (!resolved.isValid())
434 return false;
435 }
436 }
437 }
438
439 parse.results.year = year;
440 }
441 }
442
443 if (parse.results.hour < 0 && parse.hourMod12 > 0) {
444 Q_ASSERT(parse.hourMod12 <= 12);
445 parse.results.hour = parse.hourMod12 < 12 || parse.periodInDay < 0 ? parse.hourMod12 : 0;
446 if (parse.periodInDay > 0)
447 parse.results.hour += 12;
448 }
449
450 if (parse.results.year && parse.results.month && parse.results.dayOfMonth
451 && parse.results.zone.isValid() && parse.results.hour >= 0) {
452 // Should be able to construct a datetime with this:
453 const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
454 calendar);
455 Q_ASSERT(date.isValid()); // Should be ensured by earlier checks.
456 const QTime time = parse.results.time(QTime());
457 Q_ASSERT(time.isValid()); // Should be ensured by earlier checks.
458
459 // Is the given time in a transition of the given zone, on the given date ?
460 if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
461 QDateTime::TransitionResolution::Reject).isValid())) {
462 // Ambiguity, gap or outright borkage.
463 using Flaw = PartialParse::Flaw;
464 QDateTime dt(date, time, parse.results.zone, parse.results.resolveType());
465 if (!dt.isValid()) {
466 // Fall back to default resolution (same as LegacyBehavior):
467 dt = QDateTime(date, time, parse.results.zone);
468 // If that succeeded, Abbreviated (bad); otherwise Narrow (worse).
469 parse.wanton |= dt.isValid() ? Flaw::LegacyResolves : Flaw::Irreconcilable;
470 }
471 if (dt.date() != date || dt.time() != time
472 || dt.timeRepresentation() != parse.results.zone) {
473 // OK, resolution *worked* but didn't get exactly what we asked
474 // for (presumably a spring-forward's gap):
475 parse.wanton |= Flaw::ResolutionChanges;
476 // ... but we don't change parse.results because they should
477 // reflect what parsing learned; the caller can rediscover this.
478 } else {
479 // We got what we asked for (presumably the expected branch of a
480 // fall-back):
481 parse.wanton |= Flaw::SelfResolved;
482 }
483 }
484 }
485
486 if (parse.results.hour < 0) {
487 // Leave ParsedTemporal::time() a clue to am/pm, if known:
488 if (parse.periodInDay > 0)
489 parse.results.hour = PartialParse::UnknownPmHour;
490 else if (parse.periodInDay == 0)
491 parse.results.hour = PartialParse::UnknownAmHour;
492 }
493 return true;
494}
495
496std::vector<PartialParse>
497TemporalFieldMatcher::numericExtend(const PartialParse &base, QStringView text,
498 TemporalFieldFlags flags, FieldConfig &&config) const
499{
500 std::vector<PartialParse> matches;
501
502 using Flag = TemporalFieldFlag;
503 qsizetype leadingSpace = 0;
504 const bool spacePad = flags.testFlag(Flag::SpacePad);
505 if (spacePad) {
506 QStringIterator iter(text, base.results.endIndex);
507 while (iter.hasNext() && QChar::isSpace(iter.next()))
508 ++leadingSpace;
509 // If that's used up the string, the code below shall reject the field.
510 }
511
512 const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
513 locale, config.allowSign);
514 const bool zeroPad = flags.testFlag(Flag::ZeroPad);
515 // If !zeroPad, we allow < config.width but flag with Narrow in wanton fields.
516 const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
517 // This is necessarily positive: the use of chop(1) below depends on that.
518
519 QByteArrayView digits{parsed.digits};
520 // Parsed field must be representable in an int, so don't try to read more
521 // digits than an int can hold (digits10 is how many 9s in a row an int can
522 // hold; but int can hold some sequences one digit longer than that):
523 constexpr int intMaxDigits = std::numeric_limits<int>::digits10 + 1;
524 if (digits.size() > intMaxDigits)
525 digits = digits.first(intMaxDigits);
526 // Take config and flags into account, too:
527 if (config.maxDigits > 0) {
528 // Allow config.width to override config.maxDigits:
529 const int maxWidth = qMax(config.maxDigits, config.width);
530 if (digits.size() > maxWidth)
531 digits = digits.first(maxWidth);
532 } else if (flags.testFlag(Flag::YearSignIso8601) && !parsed.sign) {
533 // Limit width because a field longer than width would need a sign.
534 const int maxWidth = qMax(-config.maxDigits, config.width > 0 ? config.width : 1);
535 if (digits.size() > maxWidth)
536 digits = digits.first(maxWidth);
537 }
538 // For unbounded, work out in advance when to switch from prepending to
539 // appending; otherwise, set a cut-off that'll be true already.
540 const qsizetype appendThreshold = config.maxDigits <= 0
541 ? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
542 : digits.size();
543
544 for (; digits.size() >= width; digits.chop(1)) {
545 bool ok = false;
546 unsigned whole = digits.toUInt(&ok);
547 if (!ok)
548 continue;
549 if (config.maxValue > 0 && config.roundAfter < 0 && whole > unsigned(config.maxValue))
550 continue;
551
552 // If calendar has a year zero, we need to allow 0 in (full) year fields (width >= 4).
553 bool forbidZero = config.unset == 0 && (config.width < 4 || !calendar.hasYearZero());
554 auto optvalue = [whole, forbidZero,
555 negate = parsed.sign == '-']() -> std::optional<int> {
556 constexpr unsigned maxInt = std::numeric_limits<int>::max();
557 if (negate && whole == 1 + maxInt)
558 return std::numeric_limits<int>::min();
559 if (whole > maxInt || (forbidZero && !whole))
560 return {};
561 return negate ? -int(whole) : int(whole);
562 }();
563 if (!optvalue) // Overflow or too low
564 continue;
565 int value = *optvalue;
566
567 if (config.roundAfter >= 0) {
568 // Fractional part
569 if (digits.size() < config.roundAfter) {
570 // Interpolate omitted zero-padding up to rounding size:
571 for (int i = int(digits.size()); i < config.roundAfter; ++i)
572 value *= 10;
573 } else if (digits.size() > config.roundAfter) {
574 double v = value;
575 for (int i = int(digits.size()); i > config.roundAfter; --i)
576 v /= 10.;
577 // A timestamp that's before the end of a specified second
578 // should be rounded to the last we can before that second,
579 // especially if it's the last second of its minute, in turn
580 // especially if that's the last second of its hour (and so on).
581 value = v > config.maxValue ? config.maxValue : qRound(v);
582 // There may of course be use-cases where rounding up to the
583 // next second is desired. If it turns out those are
584 // significant, we can perhaps add a field option for it.
585 }
586 // else: exact match to number of digits, nothing to frob.
587 }
588
589 PartialParse grow = base;
590 int &target = config.target(grow);
591 if (target <= config.unset) // If unset, store:
592 target = value;
593 else if (target != value) // Conflicts with earlier field: skip this reading.
594 continue;
595 grow.results.endIndex = parsed.digitStart + digits.size() * parsed.digitWidth;
596
597 if (!zeroPad && digits.size() > qMax(1, config.width)
598 && (config.roundAfter < 0 ? digits.startsWith('0') : digits.endsWith('0'))) {
599 grow.wanton |= PartialParse::Flaw::ZeroPad;
600 }
601 if (digits.size() + leadingSpace < config.width) // (can only happen if !zeroPad)
602 grow.wanton |= PartialParse::Flaw::Narrow;
603
604 // Entries in matches are all longer than this one, as we're reducing
605 // digits. Mostly we want shorter after longer, but (for example) we
606 // prefer 4-digit years over longer matches.
607 if (digits.size() > appendThreshold)
608 matches.insert(matches.begin(), std::move(grow));
609 else
610 matches.push_back(std::move(grow));
611 }
612 return matches;
613}
614
615/* Some month names may be prefixes of others.
616 For example, the English long forms of Islamic calendar month names include:
617 * RabiÊ» I, RabiÊ» II
618 * Jumada I, Jumada II
619 Their short-forms are likewise:
620 * Rab. I, Rab. II
621 * Jum. I, Jum. II
622 In each case, one month name is a prefix of the next month's name.
623
624 In any sane format, greedy parsing shall suffice but ill-considered formats
625 happen. So the initial parse recognizes every possible match and we sort out
626 any mistakes greed might make as we parse later fields.
627*/
628std::vector<PartialParse>
629TemporalFieldMatcher::monthNameExtend(const PartialParse &base, QStringView text,
630 TemporalFieldFlags flags) const
631{
632 std::vector<PartialParse> matches;
633 using Flag = TemporalFieldFlag;
634
635 const auto addIfMatch = [&matches, &base, text, flags](int month, const QString &name) {
636 // tryEachMonth() has ensured this:
637 Q_ASSERT(!base.results.month || base.results.month == month);
638 if (name.isEmpty()) // Locale doesn't know this month's name.
639 return;
640 // If matchesAt(), add to matches:
641 auto match = matchesAt(text, base.results.endIndex, name, flags);
642 if (match) {
643 matches.emplace_back(base, match).results.month = month;
644 if (flags.testFlag(Flag::SpacePad))
645 matches = spacePadExtend(std::move(matches), text);
646 }
647 };
648
649 constexpr auto Forms = FieldGroup::FormMask;
650 constexpr int noYear = QCalendar::Unspecified;
651 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
652 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
653 const int year = base.results.year ? *base.results.year : noYear;
654 // We could try to take account of baseYear, when yearWithinCentury is
655 // known, but that's susceptible to tweaks and perturbation from other
656 // fields, so stick with noYear and the usual naming of months if we don't
657 // know year. We can consider adding a QCalendar::parseMonthName() that can
658 // consult the internal lists of localized month names, both for efficiency
659 // and to ensure we try all names, including those that appear only in some
660 // years. If we do that, its return should package month number, whether the
661 // month appears in all years and whether it was standalone or plain, along
662 // with the start and end indices of the match within the text.
663 const auto tryEachNameType = [&](QLocale::FormatType form, int month) {
664 if (lone)
665 addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
666 if (verb)
667 addIfMatch(month, calendar.monthName(locale, month, year, form));
668 };
669 // This could in principle, for non-system locales, be done more efficiently
670 // by walking the internal ';'-joined list of month names QCalendarBackend
671 // can give us. The entanglement between QCalendarBackend and QLocale
672 // internals is, however, already quite untidy enough, so leave that for
673 // if/when we discover it's a significant bottle-neck and/or we've unpicked
674 // the existing entanglement a bit first.
675
676 const auto tryEachMonth = [month = base.results.month,
677 bound = calendar.maximumMonthsInYear(),
678 &tryEachNameType](QLocale::FormatType form) {
679 if (month > 0) {
680 tryEachNameType(form, month);
681 } else {
682 for (int i = bound; i > 0; --i)
683 tryEachNameType(form, i);
684 }
685 };
686 forEachLocaleFormat(flags, tryEachMonth);
687
688 return matches;
689}
690
691std::vector<PartialParse>
692TemporalFieldMatcher::dayNameExtend(const PartialParse &base, QStringView text,
693 TemporalFieldFlags flags) const
694{
695 std::vector<PartialParse> matches;
696 using Flag = TemporalFieldFlag;
697
698 const auto addIfMatch = [&matches, &base, text, flags](int dow, const QString &name) {
699 // tryEachDayOfWeek() has ensured this:
700 Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
701 if (name.isEmpty()) // Locale doesn't know this day of the week's name.
702 return;
703 // If matchesAt(), add to matches:
704 auto match = matchesAt(text, base.results.endIndex, name, flags);
705 if (match) {
706 matches.emplace_back(base, match).results.dayOfWeek = dow;
707 if (flags.testFlag(Flag::SpacePad))
708 matches = spacePadExtend(std::move(matches), text);
709 }
710 };
711
712 constexpr auto Forms = FieldGroup::FormMask;
713 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
714 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
715 const auto tryEachNameType = [&](QLocale::FormatType form, int dow) {
716 if (lone)
717 addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
718 if (verb)
719 addIfMatch(dow, calendar.weekDayName(locale, dow, form));
720 };
721 // As for month names (see above), some collaboration with QCalendarBackend
722 // might make this more efficient for non-system locales, at the expense of
723 // adding to the existing tangle of complexity.
724
725 const auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
726 &tryEachNameType](QLocale::FormatType form) {
727 if (dow > 0) {
728 tryEachNameType(form, dow);
729 } else {
730 // Iterate possible day numbers. Issue: some calendars might have
731 // intercalary days with numbers > 7. When that happens, we may
732 // need to let this run past 7 until it's seen some empty answers.
733 for (int i = 1; i <= 7; ++i)
734 tryEachNameType(form, i);
735 }
736 };
737 forEachLocaleFormat(flags, tryEachDayOfWeek);
738
739 std::sort(matches.begin(), matches.end(), longerEarlier);
740 return matches;
741}
742
743std::pair<qsizetype, int>
744TemporalFieldMatcher::dayPeriodPrefix(const PartialParse &base, QStringView text,
745 TemporalFieldFlags flags) const
746{
747 std::pair<qsizetype, int> result = {0, -1};
748 for (int i = 0; i < 2; ++i) {
749 if (base.periodInDay >= 0 && base.periodInDay != i)
750 continue;
751 if (const QString token = i ? locale.pmText() : locale.amText(); !token.isEmpty()) {
752 if (auto match = matchesAt(text, base.results.endIndex, token, flags);
753 match.endIndex > result.first) {
754 result = { match.endIndex, i };
755 }
756 }
757 }
758 return result;
759}
760
761/*!
762 \internal
763 Find all matches to \a field, within \a text, that extend \a base.
764
765 Each match must begin at offset \c{base.results.endIndex} within \a text.
766 For each match, update a copy of \a base with the match's result, to include
767 in the returned list.
768
769 May use \c calendar to determine the range of values allowed for field.
770 Does not attempt to determine consistency between fields; see resolve() and
771 isSelfConsistent() for that. Updates the copy's member holding the value
772 described by \a field to reflect the match.
773
774 Ignores base.result.startIndex and base.result.bounds and updates each
775 copy's .endIndex to reflect the end of the match. (This leaves the caller to
776 decide whether to transfer that to .bounds.)
777
778 For fields that allow space padding, this consumes leading space as
779 necessary to make a match and includes a match for each end position at
780 which it could end; before any dangling space and after each space that
781 follows. Later calls to \c continuations() shall filter out any earlier
782 matches that precludes later fields matching just after its end. Successive
783 space-padded fields surrounded by large amounts of space are apt to lead to
784 many matches, as are final space-padded fields followed by large amounts of
785 space. (TODO: we can almost certainly mitigate this with a trivial
786 heuristic, once everything is working.)
787
788 For those matches with various flaws, relative to the field specification
789 (such as using zero padding when not obliged to), the copy's .wanton records
790 that flaw.
791
792 Sort order of the returned list should put entries likely to represent more
793 suitable matches (ignoring .wanton complications) earlier. For most fields,
794 that means longer matches come first. (For full year field matches with > 4
795 digits, though, that reverses.)
796*/
798TemporalFieldMatcher::continuations(const PartialParse &base, QStringView text,
799 const TemporalField &field) const
800{
801 std::vector<PartialParse> matches;
802 const qsizetype textPos = base.results.endIndex;
803 switch (field.category) {
804 using Cat = TemporalFieldCategory;
805 using Flag = TemporalFieldFlag;
806 case Cat::Literal:
807 if (auto match = matchesAt(text, textPos, field.literal, field.options)) {
808 matches.emplace_back(base, match);
809 if (field.options.testFlag(Flag::SpacePad))
810 matches = spacePadExtend(std::move(matches), text);
811 }
812 break;
813 case Cat::TimeZone:
814 if (const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
815 !zones.isEmpty()) {
816 for (const auto &match : zones) {
817 matches.emplace_back(base, match);
818 if (field.options.testFlag(Flag::SpacePad))
819 matches = spacePadExtend(std::move(matches), text);
820 }
821 }
822 break;
823
824 // case Cat::MillisecondInDay: break;
825 case Cat::SecondFraction:
826 matches = numericExtend(base, text, field.options,
827 {millisTarget, 999, -1, field.width, 0, 3});
828 break;
829 case Cat::Second:
830 matches = numericExtend(base, text, field.options, {secondTarget, 59, -1, field.width, 2});
831 break;
832 // case Cat::MinuteFraction: break;
833 case Cat::Minute:
834 matches = numericExtend(base, text, field.options, {minuteTarget, 59, -1, field.width, 2});
835 break;
836 // case Cat::HourFraction: break;
837 case Cat::HourMod12:
838 matches = numericExtend(base, text, field.options,
839 {hourMod12Target, 12, 0, field.width, 2});
840 break;
841 case Cat::Hour:
842 matches = numericExtend(base, text, field.options, {hourTarget, 23, -1, field.width, 2});
843 break;
844 case Cat::PeriodInDay: // am/pm; LDML also has noon, midnight, "at night" and others.
845 if (const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
846 // Ensured by dayPeriodPrefix:
847 Q_ASSERT(base.periodInDay < 0 || base.periodInDay == match.second);
848 PartialParse &grow = matches.emplace_back(base);
849 grow.results.endIndex = match.first;
850 grow.periodInDay = match.second;
851 if (field.options.testFlag(Flag::SpacePad))
852 matches = spacePadExtend(std::move(matches), text);
853 }
854 break;
855
856 case Cat::DayOfWeek:
857 matches = dayNameExtend(base, text, field.options);
858 break;
859 case Cat::DayOfMonth: {
860 const int maxDays = calendar.maximumDaysInMonth();
861 matches = numericExtend(base, text, field.options,
862 {dayOfMonthTarget, maxDays, 0, field.width,
863 maxDays < 10 ? 1 : maxDays < 100 ? 2 : 3});
864 }
865 break;
866 // case Cat::DayOfYear: break;
867 // case Cat::JulianDay: break;
868 // case Cat::WeekOfMonth: break;
869 // case Cat::WeekOfYear: break;
870 case Cat::Month:
871 // Verbal and Standalone, in so far as supported:
872 matches = monthNameExtend(base, text, field.options);
873 if (matchesFlagWithin(field.options, Flag::Numeric, FieldGroup::FormMask)) {
874 auto extend = numericExtend(base, text, field.options,
875 {monthTarget, calendar.maximumMonthsInYear(),
876 0, field.width, 2});
877 if (matches.empty())
878 matches = std::move(extend);
879 else
880 matches.insert(matches.end(), extend.begin(), extend.end());
881 }
882 std::sort(matches.begin(), matches.end(), longerEarlier);
883 break;
884 // case Cat::Quarter: break;
885 case Cat::YearWithinCentury:
886 matches = numericExtend(base, text, field.options,
887 {yearWithinCenturyTarget, 99, -1, field.width, 2});
888 break;
889 case Cat::Year:
890 matches = numericExtend(base, text, field.options,
891 {yearTarget, 0, 0, field.width, -4, -1, calendar.isProleptic()});
892 break;
893 // case Cat::RelatedGregorianYear: break;
894 // case Cat::Century: break;
895 // case Cat::Era: break;
896 }
897 return matches;
898}
899
900} // nameless namespace
901
904{
905 if (defaults.isValid()) {
908
910 // Defaults conflict with parsed day of the week.
911 if (!dayOfMonth) {
912 // (Assumes no intercalary days.)
913 // Number of days to the nearest with the right day of the week:
914 const int offset = (dayOfWeek + 10 - draft.dayOfWeek(cal)) % 7 - 3;
915 Q_ASSERT(offset != 0); // Otherwise, day of week matched, already.
916 Q_ASSERT(-4 < offset && offset < 4);
917 // Prefer closer unless nearby has more in common with what we asked for:
919 QDate nearby = draft.addDays(offset < 0 ? offset + 7 : offset - 7);
920 if (nearby.isValid()
921 && (!closer.isValid()
924 || (closer.month(cal) != draft.month(cal)
925 && nearby.month(cal) == draft.month(cal)))) {
926 // (We could also give year(cal) the same treatment, but
927 // different year, for dates within ten days of one another,
928 // plies different month, so check would be redundant.)
929 std::swap(nearby, draft);
930 } else if (closer.isValid() && closer.dayOfWeek(cal) == dayOfWeek) {
931 std::swap(closer, draft);
932 }
933
934 } else if (!month) {
936 auto use = [&draft, cal, dow=dayOfWeek](int yr, int mon, int day) {
937 QDate maybe(yr, mon, day, cal);
938 if (!maybe.isValid() || maybe.dayOfWeek(cal) != dow)
939 return false;
940 std::swap(maybe, draft);
941 return true;
942 };
943 // Find nearest month with the right dayOfMonth and dayOfWeek.
944 // If year was specified we're limited to it; otherwise,
945 // draft.year() is derived from defaults so the search can
946 // spread to nearby years.
947 int loYear = draft.year(cal), hiYear = loYear;
948 int loMon = draft.month(cal), hiMon = loMon;
949 bool maybeLo = true, maybeHi = true;
950 while (maybeLo || maybeHi) {
951 if (maybeHi) {
952 if (hiMon < cal.monthsInYear(hiYear)) {
953 ++hiMon;
954 } else if (year) {
955 Q_ASSERT(hiYear == *year);
956 maybeHi = false;
957 } else if (hiYear + 1 || cal.hasYearZero()) {
958 ++hiYear;
959 hiMon = 1;
960 } else if (cal.isProleptic()) {
961 hiYear = +1;
962 hiMon = 1;
963 } else {
964 maybeHi = false;
965 }
966 }
968 break;
969
970 if (maybeLo) {
971 if (loMon > 1) {
972 --loMon;
973 } else if (year) {
974 Q_ASSERT(loYear == *year);
975 maybeLo = false;
976 } else if (loYear - 1 || cal.hasYearZero()) {
977 --loYear;
979 } else if (cal.isProleptic()) {
980 loYear = -1;
982 } else {
983 maybeLo = false;
984 }
985 }
987 break;
988
989 // Avoid looping for ever: if we can't find a match within a
990 // 30 year window we probably never shall. If we haven't
991 // found a match by then, the likelihood that the input has
992 // a typo in it is fairly high, in any case.
993 if (hiYear - loYear > 30)
994 break;
995 }
996 } else if (!year) {
997 // As for resolve()'s handling of two-digit centuries:
1001 std::swap(maybe, draft);
1002 }
1003 if (draft.dayOfWeek(cal) != dayOfWeek)
1004 return {};
1005 }
1006 return draft;
1007 }
1008 if (year && month && dayOfMonth)
1009 return QDate(*year, month, dayOfMonth, cal);
1010 return {};
1011}
1012
1014{
1015 if (defaults.isValid()) {
1016 int hr = defaults.hour();
1017 // hour: -1 means we have no information, less means unknown am, > 24 means unknown pm.
1018 if (hour < -1) // UnknownAmHour
1019 hr = hr % 12;
1020 else if (hour > 24) // UnknownPmHour
1021 hr = hr % 12 + 12;
1022 else if (hour >= 0)
1023 hr = hour;
1024 // (Note: hour == 24 is currently unused but may be relevant for 24:00:00 in future.)
1025 return QTime(hr,
1026 minute < 0 ? defaults.minute() : minute,
1027 second < 0 ? defaults.second() : second,
1028 millis < 0 ? defaults.msec() : millis);
1029 }
1030
1031 if (hour < 0 || hour > 24)
1032 return {};
1033 if (minute < 0)
1034 return QTime(hour, 0);
1035 if (second < 0)
1036 return QTime(hour, minute);
1037 if (millis < 0)
1038 return QTime(hour, minute, second);
1039 return QTime(hour, minute, second, millis);
1040}
1041
1042ParsedTemporal prefix(QStringView text, QSpan<const QtTemporalPattern::TemporalField> fields,
1043 const QLocale &locale, QCalendar cal,
1044 std::optional<int> baseYear, qsizetype from)
1045{
1046 if (from < 0 || from >= text.size())
1047 return {};
1048
1049 const TemporalFieldMatcher matcher(locale, cal, baseYear);
1050 // Technically this is the correct (empty) result when fields.isEmpty():
1051 std::vector<PartialParse> maybe;
1052 maybe.emplace_back(from);
1053
1054 qsizetype toCome = fields.size();
1055 for (const QtTemporalPattern::TemporalField &field : fields) {
1056 --toCome;
1057 const std::vector<PartialParse> prior = std::exchange(maybe, {});
1058 for (const PartialParse &base : prior) {
1059 std::vector<PartialParse> more
1060 = matcher.continuations(base, text, field);
1061 for (PartialParse &candidate : more) {
1062 // Consistency won't have been changed by a literal field:
1063 if ((field.category == TemporalFieldCategory::Literal
1064 || matcher.isSelfConsistent(candidate, field.category))) {
1065 if (toCome) // Earlier fields' ends go in bounds:
1066 candidate.results.bounds.push_back(candidate.results.endIndex);
1067 else if (!matcher.resolve(candidate)) // Last field: makes sense of it all.
1068 continue;
1069 maybe.push_back(std::move(candidate));
1070 }
1071 }
1072 }
1073 if (maybe.empty()) // No point continuing
1074 return {};
1075 }
1076 // Now select our most favourable entry from maybe.
1077
1078 // Although we've, thus far, prefered sensible-length matches over longer
1079 // ones in individual numeric fields, so that later numeric fields can take
1080 // up the slack and win, we still want to be greedy over-all, so prefer
1081 // overall longer matches to shorter ones. None the less, between matches of
1082 // equal length, preserve our preference, up to now, for sane lengths of
1083 // each field within that, as long as later fields are taking up the slack.
1084 // That preference can be fine-tuned via .wanton, see PartialParse::Flaw.
1085 PartialParse best = maybe.front();
1086 for (const PartialParse &match : QSpan{maybe}.sliced(1)) {
1087 if (match.compare(best) < 0)
1088 best = match;
1089 }
1090 return best.results;
1091}
1092
1093} // QtParseTemporal
1094
1095QT_END_NAMESPACE
bool isSelfConsistent(const PartialParse &parsed, TemporalFieldCategory category) const
bool resolve(PartialParse &parsed) const
std::vector< PartialParse > continuations(const PartialParse &base, QStringView text, const TemporalField &field) const
TemporalFieldMatcher(const QLocale &loc, QCalendar cal, std::optional< int > centuryStart)
bool longerEarlier(const PartialParse &left, const PartialParse &right)
std::vector< PartialParse > spacePadExtend(std::vector< PartialParse > matched, QStringView text)
void forEachLocaleFormat(TemporalFieldFlags flags, qxp::function_ref< void(QLocale::FormatType) const > action)
QtParseCommon::ParsedText matchesAt(QStringView text, qsizetype from, const QString &sought, TemporalFieldFlags flags)
ParsedTemporal prefix(QStringView text, QSpan< const QtTemporalPattern::TemporalField > fields, const QLocale &locale, QCalendar cal, std::optional< int > baseYear, qsizetype from)
Qt::weak_ordering compare(const PartialParse &alt) const noexcept
PartialParse(const PartialParse &base, const QtParseCommon::ParsedText &more)