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"
12#include <QtCore/qxpfunctional.h>
20using namespace QtParseTemporal;
21using namespace QtTemporalPattern;
45 Q_DECLARE_FLAGS(Flaws,
Flaw)
49 PartialParse(qsizetype from) { results.startIndex = results.endIndex = from; }
54 Q_ASSERT(results.endIndex == more.startIndex);
55 results.endIndex = more.endIndex;
60 results.zone = more.zone;
61 results.timeType = more.timeType;
70 const auto order = [better = alt.wanton & ~wanton,
71 worse = wanton & ~alt.wanton](
Flaw test) {
72 Q_ASSERT(!(worse & better));
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;
95 if (
auto res = Qt::compareThreeWay(alt.results.size(), results.size()); res != 0)
106 return Qt::weak_ordering::equivalent;
111QLocaleData::DigitSequence
112parseDigitSequence(QStringView text, qsizetype from,
const QLocale &locale,
bool allowSign)
114 const auto *
const data = QLocalePrivate::get(locale)->m_data;
115 using DS = QLocaleData::DigitSequence;
118 flags.setFlag(DS::Option::AllowSign,
true);
119 return data->digitSequence(text, flags, from);
126 Q_ASSERT(!matched.empty());
132 const qsizetype position = matched.size() - 1;
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);
144 TemporalFieldFlags flags)
146 using F = TemporalFieldFlag;
147 const bool allowLeadingSpace = flags.testFlag(F::SpacePad);
148 Q_ASSERT(sought.size() > 0);
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) {
160 const auto matchFront = [cs](QStringView view, QStringView target) -> qsizetype {
161 if (target.isEmpty())
163 if (view.startsWith(target, cs)) {
164 qsizetype length = target.size();
168 while (view.first(length - 1).startsWith(target, cs))
172 while (!view.first(length).startsWith(target, cs))
174 Q_ASSERT(length > 0);
179 const auto spacingForward = [](QStringIterator &iter) {
184 }
while (iter.hasNext() && QChar::isSpace(iter.next()));
187 const auto matchingSpace = [](
char32_t need,
char32_t have) {
188 constexpr char32_t ASCIImax = 0x7f;
190 if (need == U' ' || need > ASCIImax)
191 return have == U' ' || have > ASCIImax;
196 constexpr qsizetype failed = 0;
197 qsizetype matched = 0;
198 if (flex || !strict) {
199 QStringIterator iter(target);
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));
208 QStringIterator viter(view, same);
210 if (!viter.hasNext())
212 if (
const char32_t got = viter.next(); !QChar::isSpace(got))
215 same = spacingForward(viter);
216 else if (!matchingSpace(tgt, got))
219 same = viter.index();
221 view = view.sliced(same);
222 target = target.sliced(flex ? spacingForward(iter) : iter.index());
223 iter = QStringIterator(target);
229 const qsizetype tail = matchFront(view, target);
232 return matched + tail;
236 qsizetype offset = 0;
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)) {
245 if (flags.testFlag(F::LowerCase)) {
246 if (qsizetype match = beginLength(view, sought.toLower()))
247 return {from, from + offset + match};
249 if (flags.testFlag(F::UpperCase)) {
250 if (qsizetype match = beginLength(view, sought.toUpper()))
251 return {from, from + offset + match};
254 }
else if (qsizetype match = beginLength(view, sought)) {
255 return {from, from + offset + match};
259 if (!allowLeadingSpace) {
265 QStringIterator iter(text.sliced(from), offset);
266 if (!iter.hasNext() || !QChar::isSpace(iter.next()))
269 Q_ASSERT(iter.index() > offset);
270 offset = iter.index();
271 }
while (text.size() >= offset + sought.size() / 2);
280 return left.results.endIndex > right.results.endIndex;
284 qxp::function_ref<
void(QLocale::FormatType)
const> action)
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);
298 const QLocale locale;
299 const QCalendar calendar;
300 const std::optional<
int> baseYear;
312 qsizetype maxDigits = 0;
314 qsizetype roundAfter = -1;
315 bool allowSign =
false;
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; }
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; }
328 if (!grow.results.year)
329 grow.results.year = 0;
330 return *grow.results.year;
334 std::vector<PartialParse>
335 numericExtend(
const PartialParse &base, QStringView text,
336 TemporalFieldFlags flags, FieldConfig &&config)
const;
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;
351 const TemporalField &field)
const;
357 TemporalFieldCategory category)
const
362 using Cat = TemporalFieldCategory;
363 if (category == Cat::Literal)
366 const bool newYear = category == Cat::Year || category == Cat::YearWithinCentury;
372 const bool newDate = (newYear || category == Cat::Month || category == Cat::DayOfMonth
373 || category == Cat::DayOfWeek);
374 if (newDate && parse.results.month && parse.results.dayOfMonth) {
376 if (parse.results.year) {
377 if (!calendar.isDateValid(*parse.results.year, parse.results.month,
378 parse.results.dayOfMonth)) {
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)
387 }
else if (calendar.daysInMonth(parse.results.month) < parse.results.dayOfMonth) {
392 if ((category == Cat::PeriodInDay && parse.results.hour >= 0)
393 || (category == Cat::Hour && parse.periodInDay >= 0)) {
395 if (parse
.periodInDay ? parse.results.hour < 12 : parse.results.hour >= 12)
399 if ((category == Cat::Hour && parse.hourMod12 > 0)
400 || (category == Cat::HourMod12 && parse.results.hour >= 0)) {
401 if ((parse.results.hour - parse
.hourMod12) % 12)
414 if (parse.results.year) {
417 }
else if (baseYear) {
418 const auto baseSplit =QRoundingDown::qDivMod<100>(*baseYear);
423 if (parse.results.month) {
426 const auto enough = [dom = parse.results.dayOfMonth](
int dim) {
427 return dim > 0 && (!dom || dom <= dim);
429 if (!enough(calendar.daysInMonth(parse.results.month, year))) {
432 for (
int off = 1; off < 10; ++off) {
433 int offset = off * 100;
434 if (enough(calendar.daysInMonth(parse.results.month, year + offset))) {
439 if (enough(calendar.daysInMonth(parse.results.month, year - offset))) {
450 if (parse.results.dayOfMonth) {
451 if (parse.results.dayOfWeek) {
452 QCalendar::YearMonthDay ymd
453 = { year, parse.results.month, parse.results.dayOfMonth };
455 = calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
456 if (!resolved.isValid())
458 year = resolved.year(calendar);
460 const QDate resolved(year, parse.results.month, parse.results.dayOfMonth);
461 if (!resolved.isValid())
467 parse.results.year = year;
471 if (parse.results.hour < 0 && parse
.hourMod12 > 0) {
475 parse.results.hour += 12;
478 if (parse.results.year && parse.results.month && parse.results.dayOfMonth
479 && parse.results.zone.isValid() && parse.results.hour >= 0) {
481 const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
483 Q_ASSERT(date.isValid());
484 const QTime time = parse.results.time(QTime());
485 Q_ASSERT(time.isValid());
488 if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
489 QDateTime::TransitionResolution::Reject).isValid())) {
492 QDateTime dt(date, time, parse.results.zone, parse.results.resolveType());
495 dt = QDateTime(date, time, parse.results.zone);
499 if (dt.date() != date || dt.time() != time
500 || dt.timeRepresentation() != parse.results.zone) {
514 if (parse.results.hour < 0) {
524std::vector<PartialParse>
526 TemporalFieldFlags flags, FieldConfig &&config)
const
528 std::vector<PartialParse> matches;
530 using Flag = TemporalFieldFlag;
531 qsizetype leadingSpace = 0;
532 const bool spacePad = flags.testFlag(Flag::SpacePad);
534 QStringIterator iter(text, base.results.endIndex);
535 while (iter.hasNext() && QChar::isSpace(iter.next()))
540 const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
541 locale, config.allowSign);
542 const bool zeroPad = flags.testFlag(Flag::ZeroPad);
544 const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
547 QByteArrayView digits{parsed.digits};
551 constexpr int intMaxDigits = std::numeric_limits<
int>::digits10 + 1;
552 if (digits.size() > intMaxDigits)
553 digits = digits.first(intMaxDigits);
555 if (config.maxDigits > 0) {
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) {
562 const int maxWidth = qMax(-config.maxDigits, config.width > 0 ? config.width : 1);
563 if (digits.size() > maxWidth)
564 digits = digits.first(maxWidth);
568 const qsizetype appendThreshold = config.maxDigits <= 0
569 ? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
572 for (; digits.size() >= width; digits.chop(1)) {
574 unsigned whole = digits.toUInt(&ok);
577 if (config.maxValue > 0 && config.roundAfter < 0 && whole >
unsigned(config.maxValue))
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))
589 return negate ? -
int(whole) :
int(whole);
593 int value = *optvalue;
595 if (config.roundAfter >= 0) {
597 if (digits.size() < config.roundAfter) {
599 for (
int i =
int(digits.size()); i < config.roundAfter; ++i)
601 }
else if (digits.size() > config.roundAfter) {
603 for (
int i =
int(digits.size()); i > config.roundAfter; --i)
609 value = v > config.maxValue ? config.maxValue : qRound(v);
618 int &target = config.target(grow);
619 if (target <= config.unset)
621 else if (target != value)
623 grow.results.endIndex = parsed.digitStart + digits.size() * parsed.digitWidth;
625 if (!zeroPad && digits.size() > qMax(1, config.width)
626 && (config.roundAfter < 0 ? digits.startsWith(
'0') : digits.endsWith(
'0'))) {
629 if (digits.size() + leadingSpace < config.width)
635 if (digits.size() > appendThreshold)
636 matches.insert(matches.begin(),
std::move(grow));
638 matches.push_back(
std::move(grow));
644
645
646
647
648
649
650
651
652
653
654
655
656std::vector<PartialParse>
658 TemporalFieldFlags flags)
const
660 std::vector<PartialParse> matches;
661 using Flag = TemporalFieldFlag;
663 const auto addIfMatch = [&matches, &base, text, flags](
int month,
const QString &name) {
665 Q_ASSERT(!base.results.month || base.results.month == month);
669 auto match = matchedAt(text, base.results.endIndex, name, flags);
671 matches.emplace_back(base, match).results.month = month;
672 if (flags.testFlag(Flag::SpacePad))
673 matches = spacePadExtend(std::move(matches), text);
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;
691 const auto tryEachNameType = [&](QLocale::FormatType form,
int month) {
693 addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
695 addIfMatch(month, calendar.monthName(locale, month, year, form));
704 const auto tryEachMonth = [month = base.results.month,
705 bound = calendar.maximumMonthsInYear(),
706 &tryEachNameType](QLocale::FormatType form) {
708 tryEachNameType(form, month);
710 for (
int i = bound; i > 0; --i)
711 tryEachNameType(form, i);
714 forEachLocaleFormat(flags, tryEachMonth);
719std::vector<PartialParse>
721 TemporalFieldFlags flags)
const
723 std::vector<PartialParse> matches;
724 using Flag = TemporalFieldFlag;
726 const auto addIfMatch = [&matches, &base, text, flags](
int dow,
const QString &name) {
728 Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
732 auto match = matchedAt(text, base.results.endIndex, name, flags);
734 matches.emplace_back(base, match).results.dayOfWeek = dow;
735 if (flags.testFlag(Flag::SpacePad))
736 matches = spacePadExtend(std::move(matches), text);
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) {
745 addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
747 addIfMatch(dow, calendar.weekDayName(locale, dow, form));
753 const auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
754 &tryEachNameType](QLocale::FormatType form) {
756 tryEachNameType(form, dow);
761 for (
int i = 1; i <= 7; ++i)
762 tryEachNameType(form, i);
765 forEachLocaleFormat(flags, tryEachDayOfWeek);
771std::pair<qsizetype,
int>
773 TemporalFieldFlags flags)
const
775 std::pair<qsizetype,
int> result = {0, -1};
776 for (
int i = 0; i < 2; ++i) {
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 };
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
827 const TemporalField &field)
const
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:
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);
844 if (
const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
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);
855 case Cat::SecondFraction:
856 matches = numericExtend(base, text, field.options,
857 {millisTarget, 999, -1, field.width, 0, 3});
860 matches = numericExtend(base, text, field.options, {secondTarget, 59, -1, field.width, 2});
864 matches = numericExtend(base, text, field.options, {minuteTarget, 59, -1, field.width, 2});
868 matches = numericExtend(base, text, field.options,
869 {hourMod12Target, 12, 0, field.width, 2});
872 matches = numericExtend(base, text, field.options, {hourTarget, 23, -1, field.width, 2});
874 case Cat::PeriodInDay:
875 if (
const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
879 grow.results.endIndex = match.first;
881 if (field.options.testFlag(Flag::SpacePad))
882 matches = spacePadExtend(std::move(matches), text);
887 matches = dayNameExtend(base, text, field.options);
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});
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(),
908 matches =
std::move(extend);
910 matches.insert(matches.end(), extend.begin(), extend.end());
915 case Cat::YearWithinCentury:
916 matches = numericExtend(base, text, field.options,
917 {yearWithinCenturyTarget, 99, -1, field.width, 2});
920 matches = numericExtend(base, text, field.options,
921 {yearTarget, 0, 0, field.width, -4, -1, calendar.isProleptic()});
1073 const QLocale &locale, QCalendar cal,
1074 std::optional<
int> baseYear, qsizetype from)
1076 if (from < 0 || from >= text.size())
1081 std::vector<PartialParse> maybe;
1082 maybe.emplace_back(from);
1084 qsizetype toCome = fields.size();
1085 for (
const QtTemporalPattern::TemporalField &field : fields) {
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) {
1093 if ((field.category == TemporalFieldCategory::Literal
1094 || matcher.isSelfConsistent(candidate, field.category))) {
1096 candidate.results.bounds.push_back(candidate.results.endIndex);
1097 else if (!matcher.resolve(candidate))
1099 maybe.push_back(std::move(candidate));
1116 for (
const PartialParse &match : QSpan{maybe}.sliced(1)) {
1117 if (match.compare(best) < 0)
1120 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)
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)
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)