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