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)) {
206 qsizetype same = head ? matchFront(view, target.first(head)) : 0;
209 QStringIterator viter(view, same);
211 if (!viter.hasNext())
213 if (
const char32_t got = viter.next(); !QChar::isSpace(got))
216 same = spacingForward(viter);
217 else if (!matchingSpace(tgt, got))
220 same = viter.index();
222 view = view.sliced(same);
223 target = target.sliced(flex ? spacingForward(iter) : iter.index());
224 iter = QStringIterator(target);
226 Q_ASSERT(!flex || !iter.hasNext() || !QChar::isSpace(iter.peekNext()));
232 const qsizetype tail = matchFront(view, target);
235 return matched + tail;
239 qsizetype offset = 0;
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)) {
248 if (flags.testFlag(F::LowerCase)) {
249 if (qsizetype match = beginLength(view, sought.toLower()))
250 return {from, from + offset + match};
252 if (flags.testFlag(F::UpperCase)) {
253 if (qsizetype match = beginLength(view, sought.toUpper()))
254 return {from, from + offset + match};
257 }
else if (qsizetype match = beginLength(view, sought)) {
258 return {from, from + offset + match};
262 if (!allowLeadingSpace) {
268 QStringIterator iter(text.sliced(from), offset);
269 if (!iter.hasNext() || !QChar::isSpace(iter.next()))
272 Q_ASSERT(iter.index() > offset);
273 offset = iter.index();
274 }
while (text.size() >= offset + sought.size() / 2);
283 return left.results.endIndex > right.results.endIndex;
287 qxp::function_ref<
void(QLocale::FormatType)
const> action)
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);
301 const QLocale locale;
302 const QCalendar calendar;
303 const std::optional<
int> baseYear;
310 bool alreadySet =
false;
313 bool allowZero =
false;
316 qsizetype maxDigits = 0;
318 qsizetype roundAfter = -1;
321 bool allowSign =
false;
322 bool allowNegative =
false;
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; }
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; }
340 if (!grow.results.year)
341 grow.results.year = 0;
342 return *grow.results.year;
346 std::vector<PartialParse>
347 numericExtend(
const PartialParse &base, QStringView text,
348 TemporalFieldFlags flags, FieldConfig &&config)
const;
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;
363 const TemporalField &field)
const;
369 TemporalFieldCategory category)
const
374 using Cat = TemporalFieldCategory;
375 if (category == Cat::Literal)
378 const bool newYear = category == Cat::Year || category == Cat::YearWithinCentury;
384 const bool newDate = (newYear || category == Cat::Month || category == Cat::DayOfMonth
385 || category == Cat::DayOfWeek);
386 if (newDate && parse.results.month && parse.results.dayOfMonth) {
388 if (parse.results.year) {
389 if (!calendar.isDateValid(*parse.results.year, parse.results.month,
390 parse.results.dayOfMonth)) {
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)
399 }
else if (calendar.daysInMonth(parse.results.month) < parse.results.dayOfMonth) {
404 if ((category == Cat::PeriodInDay && parse.results.hour >= 0)
405 || (category == Cat::Hour && parse.periodInDay >= 0)) {
407 if (parse
.periodInDay ? parse.results.hour < 12 : parse.results.hour >= 12)
411 if ((category == Cat::Hour && parse.hourMod12 > 0)
412 || (category == Cat::HourMod12 && parse.results.hour >= 0)) {
413 if ((parse.results.hour - parse
.hourMod12) % 12)
426 if (parse.results.year) {
429 }
else if (baseYear) {
430 const auto baseSplit =QRoundingDown::qDivMod<100>(*baseYear);
435 if (parse.results.month) {
438 const auto enough = [dom = parse.results.dayOfMonth](
int dim) {
439 return dim > 0 && (!dom || dom <= dim);
441 if (!enough(calendar.daysInMonth(parse.results.month, year))) {
444 for (
int off = 1; off < 10; ++off) {
445 int offset = off * 100;
446 if (enough(calendar.daysInMonth(parse.results.month, year + offset))) {
451 if (enough(calendar.daysInMonth(parse.results.month, year - offset))) {
462 if (parse.results.dayOfMonth) {
463 if (parse.results.dayOfWeek) {
464 QCalendar::YearMonthDay ymd
465 = { year, parse.results.month, parse.results.dayOfMonth };
467 = calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
468 if (!resolved.isValid())
470 year = resolved.year(calendar);
472 const QDate resolved(year, parse.results.month, parse.results.dayOfMonth);
473 if (!resolved.isValid())
479 parse.results.year = year;
483 if (parse.results.hour < 0 && parse
.hourMod12 > 0) {
487 parse.results.hour += 12;
490 if (parse.results.year && parse.results.month && parse.results.dayOfMonth
491 && parse.results.zone.isValid() && parse.results.hour >= 0) {
493 const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
495 Q_ASSERT(date.isValid());
496 const QTime time = parse.results.time(QTime());
497 Q_ASSERT(time.isValid());
500 if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
501 QDateTime::TransitionResolution::Reject).isValid())) {
504 QDateTime dt(date, time, parse.results.zone, parse.results.resolveType());
507 dt = QDateTime(date, time, parse.results.zone);
511 if (dt.date() != date || dt.time() != time
512 || dt.timeRepresentation() != parse.results.zone) {
526 if (parse.results.hour < 0) {
536std::vector<PartialParse>
538 TemporalFieldFlags flags, FieldConfig &&config)
const
540 std::vector<PartialParse> matches;
542 using Flag = TemporalFieldFlag;
543 qsizetype leadingSpace = 0;
544 const bool spacePad = flags.testFlag(Flag::SpacePad);
546 QStringIterator iter(text, base.results.endIndex);
547 while (iter.hasNext() && QChar::isSpace(iter.next()))
552 const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
553 locale, config.allowSign);
554 if (parsed.sign ==
'-' && !config.allowNegative)
556 const bool zeroPad = flags.testFlag(Flag::ZeroPad);
558 const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
561 QByteArrayView digits{parsed.digits};
565 constexpr int intMaxDigits = std::numeric_limits<
int>::digits10 + 1;
566 if (digits.size() > intMaxDigits)
567 digits = digits.first(intMaxDigits);
569 if (config.maxDigits > 0) {
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) {
576 const int maxWidth = qMax(-config.maxDigits, config.width > 0 ? config.width : 1);
577 if (digits.size() > maxWidth)
578 digits = digits.first(maxWidth);
582 const qsizetype appendThreshold = config.maxDigits <= 0
583 ? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
586 for (; digits.size() >= width; digits.chop(1)) {
588 unsigned whole = digits.toUInt(&ok);
591 if (config.maxValue > 0 && config.roundAfter < 0 && whole >
unsigned(config.maxValue))
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))
601 return negate ? -
int(whole) :
int(whole);
605 int value = *optvalue;
607 if (config.roundAfter >= 0) {
609 if (digits.size() < config.roundAfter) {
611 for (
int i =
int(digits.size()); i < config.roundAfter; ++i)
613 }
else if (digits.size() > config.roundAfter) {
615 for (
int i =
int(digits.size()); i > config.roundAfter; --i)
621 value = v > config.maxValue ? config.maxValue : qRound(v);
630 int &target = config.target(grow);
631 if (!config.alreadySet)
633 else if (target != value)
635 grow.results.endIndex = parsed.digitBounds[digits.size()];
637 if (!zeroPad && digits.size() > qMax(1, config.width)
638 && (config.roundAfter < 0 ? digits.startsWith(
'0') : digits.endsWith(
'0'))) {
641 if (digits.size() + leadingSpace < config.width)
647 if (digits.size() > appendThreshold)
648 matches.insert(matches.begin(),
std::move(grow));
650 matches.push_back(
std::move(grow));
656
657
658
659
660
661
662
663
664
665
666
667
668std::vector<PartialParse>
670 TemporalFieldFlags flags)
const
672 std::vector<PartialParse> matches;
673 using Flag = TemporalFieldFlag;
675 const auto addIfMatch = [&matches, &base, text, flags](
int month,
const QString &name) {
677 Q_ASSERT(!base.results.month || base.results.month == month);
681 auto match = matchedAt(text, base.results.endIndex, name, flags);
683 matches.emplace_back(base, match).results.month = month;
684 if (flags.testFlag(Flag::SpacePad))
685 matches = spacePadExtend(std::move(matches), text);
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;
703 const auto tryEachNameType = [&](QLocale::FormatType form,
int month) {
705 addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
707 addIfMatch(month, calendar.monthName(locale, month, year, form));
716 const auto tryEachMonth = [month = base.results.month,
717 bound = calendar.maximumMonthsInYear(),
718 &tryEachNameType](QLocale::FormatType form) {
720 tryEachNameType(form, month);
722 for (
int i = bound; i > 0; --i)
723 tryEachNameType(form, i);
726 forEachLocaleFormat(flags, tryEachMonth);
731std::vector<PartialParse>
733 TemporalFieldFlags flags)
const
735 std::vector<PartialParse> matches;
736 using Flag = TemporalFieldFlag;
738 const auto addIfMatch = [&matches, &base, text, flags](
int dow,
const QString &name) {
740 Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
744 auto match = matchedAt(text, base.results.endIndex, name, flags);
746 matches.emplace_back(base, match).results.dayOfWeek = dow;
747 if (flags.testFlag(Flag::SpacePad))
748 matches = spacePadExtend(std::move(matches), text);
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) {
757 addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
759 addIfMatch(dow, calendar.weekDayName(locale, dow, form));
765 const auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
766 &tryEachNameType](QLocale::FormatType form) {
768 tryEachNameType(form, dow);
773 for (
int i = 1; i <= 7; ++i)
774 tryEachNameType(form, i);
777 forEachLocaleFormat(flags, tryEachDayOfWeek);
783std::pair<qsizetype,
int>
785 TemporalFieldFlags flags)
const
787 std::pair<qsizetype,
int> result = {0, -1};
788 for (
int i = 0; i < 2; ++i) {
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 };
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
839 const TemporalField &field)
const
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:
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);
856 if (
const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
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);
867 case Cat::SecondFraction:
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});
875 matches = numericExtend(base, text, field.options,
876 {secondTarget, base.results.second >= 0, 59,
true, field.width, 2});
880 matches = numericExtend(base, text, field.options,
881 {minuteTarget, base.results.minute >= 0, 59,
true, field.width, 2});
885 matches = numericExtend(base, text, field.options,
886 {hourMod12Target, base.hourMod12 > 0, 12,
false, field.width, 2});
889 matches = numericExtend(base, text, field.options,
890 {hourTarget, base.results.hour >= 0, 23,
true, field.width, 2});
892 case Cat::PeriodInDay:
893 if (
const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
897 grow.results.endIndex = match.first;
899 if (field.options.testFlag(Flag::SpacePad))
900 matches = spacePadExtend(std::move(matches), text);
905 matches = dayNameExtend(base, text, field.options);
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});
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});
926 matches =
std::move(extend);
928 matches.insert(matches.end(), extend.begin(), extend.end());
933 case Cat::YearWithinCentury:
934 matches = numericExtend(base, text, field.options,
935 {yearWithinCenturyTarget, base.yearWithinCentury >= 0,
936 99,
true, field.width, 2});
939 matches = numericExtend(base, text, field.options,
940 {yearTarget,
bool(base.results.year), 0, calendar.hasYearZero(),
941 field.width, -4, -1,
true, calendar.isProleptic()});
1093 const QLocale &locale, QCalendar cal,
1094 std::optional<
int> baseYear, qsizetype from)
1096 if (from < 0 || from >= text.size())
1101 std::vector<PartialParse> maybe;
1102 maybe.emplace_back(from);
1104 qsizetype toCome = fields.size();
1105 for (
const QtTemporalPattern::TemporalField &field : fields) {
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) {
1113 if ((field.category == TemporalFieldCategory::Literal
1114 || matcher.isSelfConsistent(candidate, field.category))) {
1116 candidate.results.bounds.push_back(candidate.results.endIndex);
1117 else if (!matcher.resolve(candidate))
1119 maybe.push_back(std::move(candidate));
1136 for (
const PartialParse &match : QSpan{maybe}.sliced(1)) {
1137 if (match.compare(best) < 0)
1140 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)