4#include "private/qtparsetemporal_p.h"
6#include "private/qcalendarmath_p.h"
7#include "private/qlocale_p.h"
8#include "private/qstringiterator_p.h"
9#include "private/qttemporalpattern_p.h"
19using namespace QtParseTemporal;
20using namespace QtTemporalPattern;
44 Q_DECLARE_FLAGS(Flaws,
Flaw)
48 PartialParse(qsizetype from) { results.startIndex = results.endIndex = from; }
53 Q_ASSERT(results.endIndex == more.startIndex);
54 results.endIndex = more.endIndex;
59 results.zone = more.zone;
60 results.timeType = more.timeType;
69 const auto order = [better = alt.wanton & ~wanton,
70 worse = wanton & ~alt.wanton](
Flaw test) {
71 Q_ASSERT(!(worse & better));
72 if (better.testFlag(test))
73 return Qt::weak_ordering::less;
74 if (worse.testFlag(test))
75 return Qt::weak_ordering::greater;
76 return Qt::weak_ordering::equivalent;
96 if (
auto res = Qt::compareThreeWay(alt.results.size(), results.size()); res != 0)
105 return Qt::weak_ordering::equivalent;
110QLocaleData::DigitSequence
111parseDigitSequence(QStringView text, qsizetype from,
const QLocale &locale,
bool allowSign)
113 const auto *
const data = QLocalePrivate::get(locale)->m_data;
114 using DS = QLocaleData::DigitSequence;
117 flags.setFlag(DS::Option::AllowSign,
true);
118 return data->digitSequence(text, flags, from);
125 Q_ASSERT(!matched.empty());
131 const qsizetype position = matched.size() - 1;
133 QStringIterator iter(text, copy.results.endIndex);
134 while (iter.hasNext() && QChar::isSpace(iter.next())) {
135 Q_ASSERT(iter.index() > copy.results.endIndex);
136 copy.results.endIndex = iter.index();
137 matched.insert(matched.begin() + position, copy);
143 TemporalFieldFlags flags)
145 using F = TemporalFieldFlag;
146 const bool allowLeadingSpace = flags.testFlag(F::SpacePad);
147 Q_ASSERT(sought.size() > 0);
154 const auto beginLength = [flex = flags.testFlag(F::FlexSpace)]
155 (QStringView view, QStringView target, Qt::CaseSensitivity cs = Qt::CaseSensitive) {
158 const auto matchFront = [cs](QStringView view, QStringView target) {
159 if (view.startsWith(target, cs)) {
160 qsizetype length = target.size();
161 while (view.first(length - 1).startsWith(target, cs))
163 while (!view.first(length).startsWith(target, cs))
165 Q_ASSERT(length > 0);
168 return qsizetype(-1);
170 const auto spaceForward = [](QStringIterator &iter) {
175 }
while (iter.hasNext() && QChar::isSpace(iter.next()));
178 constexpr qsizetype failed = 0;
179 qsizetype matched = 0;
181 QStringIterator iter(target);
182 while (iter.hasNext()) {
183 qsizetype head = iter.index();
184 if (QChar::isSpace(iter.next())) {
185 qsizetype same = head > 0 ? matchFront(view, target.first(head)) : 0;
188 QStringIterator viter(view, same);
190 if (!viter.hasNext() || !QChar::isSpace(viter.next()))
192 same = spaceForward(viter);
194 view = view.sliced(same);
195 target = target.sliced(spaceForward(iter));
196 iter = QStringIterator(target);
200 const qsizetype tail = target.isEmpty() ? 0 : matchFront(view, target);
203 return matched + tail;
207 qsizetype offset = 0;
209 QStringView view = text.sliced(from + offset);
210 if (flags.testFlag(F::IgnoreCase)) {
211 if (qsizetype match = beginLength(view, sought, Qt::CaseInsensitive))
212 return {from, from + offset + match};
213 }
else if (flags.testAnyFlags(F::LowerCase | F::UpperCase)) {
216 if (flags.testFlag(F::LowerCase)) {
217 if (qsizetype match = beginLength(view, sought.toLower()))
218 return {from, from + offset + match};
220 if (flags.testFlag(F::UpperCase)) {
221 if (qsizetype match = beginLength(view, sought.toUpper()))
222 return {from, from + offset + match};
225 }
else if (qsizetype match = beginLength(view, sought)) {
226 return {from, from + offset + match};
230 if (!allowLeadingSpace) {
236 QStringIterator iter(text.sliced(from), offset);
237 if (!iter.hasNext() || !QChar::isSpace(iter.next()))
240 Q_ASSERT(iter.index() > offset);
241 offset = iter.index();
242 }
while (text.size() >= offset + sought.size() / 2);
251 return left.results.endIndex > right.results.endIndex;
254template <
typename Action>
257 using Flag = TemporalFieldFlag;
258 constexpr auto Widths = FieldGroup::WidthMask;
259 if (matchesFlagWithin(flags, Flag::Wide, Widths))
260 action(QLocale::LongFormat);
261 if (matchesFlagsWithin(flags, Flag::Short | Flag::Abbreviated, Widths))
262 action(QLocale::ShortFormat);
263 if (matchesFlagWithin(flags, Flag::Narrow, Widths))
264 action(QLocale::NarrowFormat);
269 const QLocale locale;
270 const QCalendar calendar;
271 const std::optional<
int> baseYear;
283 qsizetype maxDigits = 0;
285 qsizetype roundAfter = -1;
286 bool allowSign =
false;
289 static int &millisTarget(
PartialParse &grow) {
return grow.results.millis; }
290 static int &secondTarget(
PartialParse &grow) {
return grow.results.second; }
291 static int &minuteTarget(
PartialParse &grow) {
return grow.results.minute; }
292 static int &hourTarget(
PartialParse &grow) {
return grow.results.hour; }
294 static int &dayOfWeekTarget(
PartialParse &grow) {
return grow.results.dayOfWeek; }
295 static int &dayOfMonthTarget(
PartialParse &grow) {
return grow.results.dayOfMonth; }
296 static int &monthTarget(
PartialParse &grow) {
return grow.results.month; }
299 if (!grow.results.year)
300 grow.results.year = 0;
301 return *grow.results.year;
305 std::vector<PartialParse>
306 numericExtend(
const PartialParse &base, QStringView text,
307 TemporalFieldFlags flags, FieldConfig &&config)
const;
310 std::vector<PartialParse> monthNameExtend(
const PartialParse &base, QStringView text,
311 TemporalFieldFlags flags)
const;
312 std::vector<PartialParse> dayNameExtend(
const PartialParse &base, QStringView text,
313 TemporalFieldFlags flags)
const;
314 std::pair<qsizetype,
int> dayPeriodPrefix(
const PartialParse &base, QStringView text,
315 TemporalFieldFlags flags)
const;
322 const TemporalField &field)
const;
328 TemporalFieldCategory category)
const
333 using Cat = TemporalFieldCategory;
334 if (category == Cat::Literal)
337 const bool newYear = category == Cat::Year || category == Cat::YearWithinCentury;
343 const bool newDate = (newYear || category == Cat::Month || category == Cat::DayOfMonth
344 || category == Cat::DayOfWeek);
345 if (newDate && parse.results.month && parse.results.dayOfMonth) {
347 if (parse.results.year) {
348 if (!calendar.isDateValid(*parse.results.year, parse.results.month,
349 parse.results.dayOfMonth)) {
352 if (parse.results.dayOfWeek) {
353 QDate date = calendar.dateFromParts(*parse.results.year, parse.results.month,
354 parse.results.dayOfMonth);
355 if (calendar.dayOfWeek(date) != parse.results.dayOfWeek)
358 }
else if (calendar.daysInMonth(parse.results.month) < parse.results.dayOfMonth) {
363 if ((category == Cat::PeriodInDay && parse.results.hour >= 0)
364 || (category == Cat::Hour && parse.periodInDay >= 0)) {
366 if (parse
.periodInDay ? parse.results.hour < 12 : parse.results.hour >= 12)
370 if ((category == Cat::Hour && parse.hourMod12 > 0)
371 || (category == Cat::HourMod12 && parse.results.hour >= 0)) {
372 if ((parse.results.hour - parse
.hourMod12) % 12)
385 if (parse.results.year) {
388 }
else if (baseYear) {
389 const auto baseSplit =QRoundingDown::qDivMod<100>(*baseYear);
394 if (parse.results.dayOfWeek && parse.results.month && parse.results.dayOfMonth) {
395 QCalendar::YearMonthDay ymd
396 = { year, parse.results.month, parse.results.dayOfMonth };
397 QDate resolved = calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
398 if (!resolved.isValid())
400 year = resolved.year(calendar);
403 parse.results.year = year;
407 if (parse.results.hour < 0 && parse
.hourMod12 > 0) {
411 parse.results.hour += 12;
414 if (parse.results.year && parse.results.month && parse.results.dayOfMonth
415 && parse.results.zone.isValid() && parse.results.hour >= 0) {
417 const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
419 Q_ASSERT(date.isValid());
420 const QTime time = parse.results.time(QTime());
421 Q_ASSERT(time.isValid());
424 if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
425 QDateTime::TransitionResolution::Reject).isValid())) {
427 const auto res = [type = parse.results.timeType]() {
428 using Res = QDateTime::TransitionResolution;
430 case QTimeZone::StandardTime:
return Res::PreferStandard;
431 case QTimeZone::DaylightTime:
return Res::PreferDaylightSaving;
432 case QTimeZone::GenericTime:
return Res::LegacyBehavior;
434 Q_UNREACHABLE_RETURN(Res::LegacyBehavior);
437 QDateTime dt(date, time, parse.results.zone, res);
440 dt = QDateTime(date, time, parse.results.zone);
444 if (dt.date() != date || dt.time() != time
445 || dt.timeRepresentation() != parse.results.zone) {
459 if (parse.results.hour < 0) {
469std::vector<PartialParse>
471 TemporalFieldFlags flags, FieldConfig &&config)
const
473 using Flag = TemporalFieldFlag;
474 qsizetype leadingSpace = 0;
475 const bool spacePad = flags.testFlag(Flag::SpacePad);
477 QStringIterator iter(text, base.results.endIndex);
478 while (iter.hasNext() && QChar::isSpace(iter.next()))
483 const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
484 locale, config.allowSign);
485 const bool zeroPad = flags.testFlag(Flag::ZeroPad);
487 const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
490 QByteArrayView digits{parsed.digits};
491 if (config.maxDigits > 0) {
493 const int maxWidth = qMax(config.maxDigits, config.width);
494 if (digits.size() > maxWidth)
495 digits = digits.first(maxWidth);
496 }
else if (flags.testFlag(Flag::YearSignIso8601) && !parsed.sign) {
498 const int maxWidth = config.width > 0 ? config.width : qMax(-config.maxDigits, 1);
499 if (digits.size() > maxWidth)
500 digits = digits.first(maxWidth);
504 const qsizetype appendThreshold = config.maxDigits <= 0
505 ? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
508 std::vector<PartialParse> matches;
509 for (; digits.size() >= width; digits.chop(1)) {
511 unsigned whole = digits.toUInt(&ok);
514 if (config.maxValue > 0 && config.roundAfter < 0 && whole >
unsigned(config.maxValue))
518 if (value < 0 || value <= config.unset)
520 if (parsed.sign ==
'-')
523 if (config.roundAfter >= 0) {
525 if (digits.size() < config.roundAfter) {
527 for (
int i =
int(digits.size()); i < config.roundAfter; ++i)
529 }
else if (digits.size() > config.roundAfter) {
531 for (
int i =
int(digits.size()); i > config.roundAfter; --i)
537 value = v > config.maxValue ? config.maxValue : qRound(v);
546 int &target = config.target(grow);
547 if (target <= config.unset)
549 else if (target != value)
551 grow.results.endIndex = parsed.digitStart + digits.size() * parsed.digitWidth;
553 if (!zeroPad && digits.size() > qMax(1, config.width)
554 && (config.roundAfter < 0 ? digits.startsWith(
'0') : digits.endsWith(
'0'))) {
557 if (digits.size() + leadingSpace < config.width)
563 if (digits.size() > appendThreshold)
564 matches.insert(matches.begin(),
std::move(grow));
566 matches.push_back(
std::move(grow));
572
573
574
575
576
577
578
579
580
581
582
583
584std::vector<PartialParse>
586 TemporalFieldFlags flags)
const
588 std::vector<PartialParse> matches;
589 using Flag = TemporalFieldFlag;
591 auto addIfMatch = [&matches, base, text, flags](
int month, QString &&name) {
593 Q_ASSERT(!base.results.month || base.results.month == month);
597 auto match = matchesAt(text, base.results.endIndex, name, flags);
600 grow.results.month = month;
601 matches.push_back(grow);
602 if (flags.testFlag(Flag::SpacePad))
603 matches = spacePadExtend(std::move(matches), text);
607 constexpr auto Forms = FieldGroup::FormMask;
608 constexpr int noYear = QCalendar::Unspecified;
609 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
610 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
611 const int year = base.results.year ? *base.results.year : noYear;
621 auto tryEachNameType = [
this, verb, lone, year,
622 addIfMatch](QLocale::FormatType form,
int month) {
624 addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
626 addIfMatch(month, calendar.monthName(locale, month, year, form));
635 auto tryEachMonth = [month = base.results.month, bound = calendar.maximumMonthsInYear(),
636 tryEachNameType](QLocale::FormatType form) {
638 tryEachNameType(form, month);
640 for (
int i = bound; i > 0; --i)
641 tryEachNameType(form, i);
644 forEachLocaleFormat(flags, tryEachMonth);
649std::vector<PartialParse>
651 TemporalFieldFlags flags)
const
653 std::vector<PartialParse> matches;
654 using Flag = TemporalFieldFlag;
656 auto addIfMatch = [&matches, base, text, flags](
int dow, QString &&name) {
658 Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
662 auto match = matchesAt(text, base.results.endIndex, name, flags);
665 grow.results.dayOfWeek = dow;
666 matches.push_back(grow);
667 if (flags.testFlag(Flag::SpacePad))
668 matches = spacePadExtend(std::move(matches), text);
672 constexpr auto Forms = FieldGroup::FormMask;
673 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
674 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
675 auto tryEachNameType = [
this, addIfMatch, verb, lone](QLocale::FormatType form,
int dow) {
677 addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
679 addIfMatch(dow, calendar.weekDayName(locale, dow, form));
685 auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
686 tryEachNameType](QLocale::FormatType form) {
688 tryEachNameType(form, dow);
693 for (
int i = 1; i <= 7; ++i)
694 tryEachNameType(form, i);
697 forEachLocaleFormat(flags, tryEachDayOfWeek);
703std::pair<qsizetype,
int>
705 TemporalFieldFlags flags)
const
707 std::pair<qsizetype,
int> result = {0, -1};
708 for (
int i = 0; i < 2; ++i) {
711 if (
const QString token = i ? locale.pmText() : locale.amText(); !token.isEmpty()) {
712 if (
auto match = matchesAt(text, base.results.endIndex, token, flags);
713 match.endIndex > result.first) {
714 result = { match.endIndex, i };
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
759 const TemporalField &field)
const
761 std::vector<PartialParse> matches;
762 const qsizetype textPos = base.results.endIndex;
763 switch (field.category) {
764 using Cat = TemporalFieldCategory;
765 using Flag = TemporalFieldFlag;
767 if (
auto match = matchesAt(text, textPos, field.literal, field.options)) {
769 if (field.options.testFlag(Flag::SpacePad))
770 matches = spacePadExtend(std::move(matches), text);
774 if (
const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
776 for (
const auto &match : zones) {
777 matches.push_back(PartialParse(base, match));
778 if (field.options.testFlag(Flag::SpacePad))
779 matches = spacePadExtend(std::move(matches), text);
785 case Cat::SecondFraction:
786 matches = numericExtend(base, text, field.options,
787 {millisTarget, 999, -1, field.width, 0, 3});
790 matches = numericExtend(base, text, field.options, {secondTarget, 59, -1, field.width, 2});
794 matches = numericExtend(base, text, field.options, {minuteTarget, 59, -1, field.width, 2});
798 matches = numericExtend(base, text, field.options,
799 {hourMod12Target, 12, 0, field.width, 2});
802 matches = numericExtend(base, text, field.options, {hourTarget, 23, -1, field.width, 2});
804 case Cat::PeriodInDay:
805 if (
const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
809 grow.results.endIndex = match.first;
811 matches.push_back(grow);
812 if (field.options.testFlag(Flag::SpacePad))
813 matches = spacePadExtend(std::move(matches), text);
818 matches = dayNameExtend(base, text, field.options);
820 case Cat::DayOfMonth: {
821 const int maxDays = calendar.maximumDaysInMonth();
822 matches = numericExtend(base, text, field.options,
823 {dayOfMonthTarget, maxDays, 0, field.width,
824 maxDays < 10 ? 1 : maxDays < 100 ? 2 : 3});
833 matches = monthNameExtend(base, text, field.options);
834 if (matchesFlagWithin(field.options, Flag::Numeric, FieldGroup::FormMask)) {
835 auto extend = numericExtend(base, text, field.options,
836 {monthTarget, calendar.maximumMonthsInYear(),
839 matches =
std::move(extend);
841 matches.insert(matches.end(), extend.begin(), extend.end());
846 case Cat::YearWithinCentury:
847 matches = numericExtend(base, text, field.options,
848 {yearWithinCenturyTarget, 99, -1, field.width, 2});
851 matches = numericExtend(base, text, field.options,
852 {yearTarget, 0, 0, field.width, -4, -1,
true});
1004 const QLocale &locale, QCalendar cal,
1005 std::optional<
int> baseYear, qsizetype from)
1007 if (from < 0 || from >= text.size())
1014 qsizetype toCome = fields.size();
1015 for (
const QtTemporalPattern::TemporalField &field : fields) {
1017 const std::vector<PartialParse> prior = std::exchange(maybe, {});
1018 for (
const PartialParse &base : prior) {
1019 std::vector<PartialParse> more
1020 = matcher.continuations(base, text, field);
1021 for (PartialParse &candidate : more) {
1023 if ((field.category == TemporalFieldCategory::Literal
1024 || matcher.isSelfConsistent(candidate, field.category))) {
1026 candidate.results.bounds.push_back(candidate.results.endIndex);
1027 else if (!matcher.resolve(candidate))
1029 maybe.push_back(std::move(candidate));
1046 for (
const PartialParse &match : QSpan{maybe}.sliced(1)) {
1047 if (match.compare(best) < 0)
1050 return best.results;
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)
void forEachLocaleFormat(TemporalFieldFlags flags, Action action)
std::vector< PartialParse > spacePadExtend(std::vector< PartialParse > &&matched, QStringView text)
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)
static constexpr int UnknownAmHour
Qt::weak_ordering compare(const PartialParse &alt) const noexcept
PartialParse(qsizetype from)
static constexpr int UnknownPmHour
PartialParse(const PartialParse &base, const QtParseCommon::ParsedText &more)