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