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;
94 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.month) {
397 const auto enough = [dom = parse.results.dayOfMonth](
int dim) {
398 return dim > 0 && (!dom || dom <= dim);
400 if (!enough(calendar.daysInMonth(parse.results.month, year))) {
403 for (
int off = 1; off < 10; ++off) {
404 int offset = off * 100;
405 if (enough(calendar.daysInMonth(parse.results.month, year + offset))) {
410 if (enough(calendar.daysInMonth(parse.results.month, year - offset))) {
421 if (parse.results.dayOfMonth) {
422 if (parse.results.dayOfWeek) {
423 QCalendar::YearMonthDay ymd
424 = { year, parse.results.month, parse.results.dayOfMonth };
426 = calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
427 if (!resolved.isValid())
429 year = resolved.year(calendar);
431 const QDate resolved(year, parse.results.month, parse.results.dayOfMonth);
432 if (!resolved.isValid())
438 parse.results.year = year;
442 if (parse.results.hour < 0 && parse
.hourMod12 > 0) {
446 parse.results.hour += 12;
449 if (parse.results.year && parse.results.month && parse.results.dayOfMonth
450 && parse.results.zone.isValid() && parse.results.hour >= 0) {
452 const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
454 Q_ASSERT(date.isValid());
455 const QTime time = parse.results.time(QTime());
456 Q_ASSERT(time.isValid());
459 if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
460 QDateTime::TransitionResolution::Reject).isValid())) {
463 QDateTime dt(date, time, parse.results.zone, parse.results.resolveType());
466 dt = QDateTime(date, time, parse.results.zone);
470 if (dt.date() != date || dt.time() != time
471 || dt.timeRepresentation() != parse.results.zone) {
485 if (parse.results.hour < 0) {
495std::vector<PartialParse>
497 TemporalFieldFlags flags, FieldConfig &&config)
const
499 using Flag = TemporalFieldFlag;
500 qsizetype leadingSpace = 0;
501 const bool spacePad = flags.testFlag(Flag::SpacePad);
503 QStringIterator iter(text, base.results.endIndex);
504 while (iter.hasNext() && QChar::isSpace(iter.next()))
509 const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
510 locale, config.allowSign);
511 const bool zeroPad = flags.testFlag(Flag::ZeroPad);
513 const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
516 QByteArrayView digits{parsed.digits};
517 if (config.maxDigits > 0) {
519 const int maxWidth = qMax(config.maxDigits, config.width);
520 if (digits.size() > maxWidth)
521 digits = digits.first(maxWidth);
522 }
else if (flags.testFlag(Flag::YearSignIso8601) && !parsed.sign) {
524 const int maxWidth = config.width > 0 ? config.width : qMax(-config.maxDigits, 1);
525 if (digits.size() > maxWidth)
526 digits = digits.first(maxWidth);
530 const qsizetype appendThreshold = config.maxDigits <= 0
531 ? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
534 std::vector<PartialParse> matches;
535 for (; digits.size() >= width; digits.chop(1)) {
537 unsigned whole = digits.toUInt(&ok);
540 if (config.maxValue > 0 && config.roundAfter < 0 && whole >
unsigned(config.maxValue))
544 if (value < 0 || value <= config.unset)
546 if (parsed.sign ==
'-')
549 if (config.roundAfter >= 0) {
551 if (digits.size() < config.roundAfter) {
553 for (
int i =
int(digits.size()); i < config.roundAfter; ++i)
555 }
else if (digits.size() > config.roundAfter) {
557 for (
int i =
int(digits.size()); i > config.roundAfter; --i)
563 value = v > config.maxValue ? config.maxValue : qRound(v);
572 int &target = config.target(grow);
573 if (target <= config.unset)
575 else if (target != value)
577 grow.results.endIndex = parsed.digitStart + digits.size() * parsed.digitWidth;
579 if (!zeroPad && digits.size() > qMax(1, config.width)
580 && (config.roundAfter < 0 ? digits.startsWith(
'0') : digits.endsWith(
'0'))) {
583 if (digits.size() + leadingSpace < config.width)
589 if (digits.size() > appendThreshold)
590 matches.insert(matches.begin(),
std::move(grow));
592 matches.push_back(
std::move(grow));
598
599
600
601
602
603
604
605
606
607
608
609
610std::vector<PartialParse>
612 TemporalFieldFlags flags)
const
614 std::vector<PartialParse> matches;
615 using Flag = TemporalFieldFlag;
617 auto addIfMatch = [&matches, base, text, flags](
int month, QString &&name) {
619 Q_ASSERT(!base.results.month || base.results.month == month);
623 auto match = matchesAt(text, base.results.endIndex, name, flags);
626 grow.results.month = month;
627 matches.push_back(grow);
628 if (flags.testFlag(Flag::SpacePad))
629 matches = spacePadExtend(std::move(matches), text);
633 constexpr auto Forms = FieldGroup::FormMask;
634 constexpr int noYear = QCalendar::Unspecified;
635 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
636 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
637 const int year = base.results.year ? *base.results.year : noYear;
647 auto tryEachNameType = [
this, verb, lone, year,
648 addIfMatch](QLocale::FormatType form,
int month) {
650 addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
652 addIfMatch(month, calendar.monthName(locale, month, year, form));
661 auto tryEachMonth = [month = base.results.month, bound = calendar.maximumMonthsInYear(),
662 tryEachNameType](QLocale::FormatType form) {
664 tryEachNameType(form, month);
666 for (
int i = bound; i > 0; --i)
667 tryEachNameType(form, i);
670 forEachLocaleFormat(flags, tryEachMonth);
675std::vector<PartialParse>
677 TemporalFieldFlags flags)
const
679 std::vector<PartialParse> matches;
680 using Flag = TemporalFieldFlag;
682 auto addIfMatch = [&matches, base, text, flags](
int dow, QString &&name) {
684 Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
688 auto match = matchesAt(text, base.results.endIndex, name, flags);
691 grow.results.dayOfWeek = dow;
692 matches.push_back(grow);
693 if (flags.testFlag(Flag::SpacePad))
694 matches = spacePadExtend(std::move(matches), text);
698 constexpr auto Forms = FieldGroup::FormMask;
699 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
700 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
701 auto tryEachNameType = [
this, addIfMatch, verb, lone](QLocale::FormatType form,
int dow) {
703 addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
705 addIfMatch(dow, calendar.weekDayName(locale, dow, form));
711 auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
712 tryEachNameType](QLocale::FormatType form) {
714 tryEachNameType(form, dow);
719 for (
int i = 1; i <= 7; ++i)
720 tryEachNameType(form, i);
723 forEachLocaleFormat(flags, tryEachDayOfWeek);
729std::pair<qsizetype,
int>
731 TemporalFieldFlags flags)
const
733 std::pair<qsizetype,
int> result = {0, -1};
734 for (
int i = 0; i < 2; ++i) {
737 if (
const QString token = i ? locale.pmText() : locale.amText(); !token.isEmpty()) {
738 if (
auto match = matchesAt(text, base.results.endIndex, token, flags);
739 match.endIndex > result.first) {
740 result = { match.endIndex, i };
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
785 const TemporalField &field)
const
787 std::vector<PartialParse> matches;
788 const qsizetype textPos = base.results.endIndex;
789 switch (field.category) {
790 using Cat = TemporalFieldCategory;
791 using Flag = TemporalFieldFlag;
793 if (
auto match = matchesAt(text, textPos, field.literal, field.options)) {
795 if (field.options.testFlag(Flag::SpacePad))
796 matches = spacePadExtend(std::move(matches), text);
800 if (
const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
802 for (
const auto &match : zones) {
803 matches.push_back(PartialParse(base, match));
804 if (field.options.testFlag(Flag::SpacePad))
805 matches = spacePadExtend(std::move(matches), text);
811 case Cat::SecondFraction:
812 matches = numericExtend(base, text, field.options,
813 {millisTarget, 999, -1, field.width, 0, 3});
816 matches = numericExtend(base, text, field.options, {secondTarget, 59, -1, field.width, 2});
820 matches = numericExtend(base, text, field.options, {minuteTarget, 59, -1, field.width, 2});
824 matches = numericExtend(base, text, field.options,
825 {hourMod12Target, 12, 0, field.width, 2});
828 matches = numericExtend(base, text, field.options, {hourTarget, 23, -1, field.width, 2});
830 case Cat::PeriodInDay:
831 if (
const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
835 grow.results.endIndex = match.first;
837 matches.push_back(grow);
838 if (field.options.testFlag(Flag::SpacePad))
839 matches = spacePadExtend(std::move(matches), text);
844 matches = dayNameExtend(base, text, field.options);
846 case Cat::DayOfMonth: {
847 const int maxDays = calendar.maximumDaysInMonth();
848 matches = numericExtend(base, text, field.options,
849 {dayOfMonthTarget, maxDays, 0, field.width,
850 maxDays < 10 ? 1 : maxDays < 100 ? 2 : 3});
859 matches = monthNameExtend(base, text, field.options);
860 if (matchesFlagWithin(field.options, Flag::Numeric, FieldGroup::FormMask)) {
861 auto extend = numericExtend(base, text, field.options,
862 {monthTarget, calendar.maximumMonthsInYear(),
865 matches =
std::move(extend);
867 matches.insert(matches.end(), extend.begin(), extend.end());
872 case Cat::YearWithinCentury:
873 matches = numericExtend(base, text, field.options,
874 {yearWithinCenturyTarget, 99, -1, field.width, 2});
877 matches = numericExtend(base, text, field.options,
878 {yearTarget, 0, 0, field.width, -4, -1,
true});
1030 const QLocale &locale, QCalendar cal,
1031 std::optional<
int> baseYear, qsizetype from)
1033 if (from < 0 || from >= text.size())
1040 qsizetype toCome = fields.size();
1041 for (
const QtTemporalPattern::TemporalField &field : fields) {
1043 const std::vector<PartialParse> prior = std::exchange(maybe, {});
1044 for (
const PartialParse &base : prior) {
1045 std::vector<PartialParse> more
1046 = matcher.continuations(base, text, field);
1047 for (PartialParse &candidate : more) {
1049 if ((field.category == TemporalFieldCategory::Literal
1050 || matcher.isSelfConsistent(candidate, field.category))) {
1052 candidate.results.bounds.push_back(candidate.results.endIndex);
1053 else if (!matcher.resolve(candidate))
1055 maybe.push_back(std::move(candidate));
1072 for (
const PartialParse &match : QSpan{maybe}.sliced(1)) {
1073 if (match.compare(best) < 0)
1076 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)