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 (QStringView view, QStringView target, Qt::CaseSensitivity cs = Qt::CaseSensitive) {
159 const auto matchFront = [cs](QStringView view, QStringView target) {
160 if (view.startsWith(target, cs)) {
161 qsizetype length = target.size();
162 while (view.first(length - 1).startsWith(target, cs))
164 while (!view.first(length).startsWith(target, cs))
166 Q_ASSERT(length > 0);
169 return qsizetype(-1);
171 const auto spaceForward = [](QStringIterator &iter) {
176 }
while (iter.hasNext() && QChar::isSpace(iter.next()));
179 constexpr qsizetype failed = 0;
180 qsizetype matched = 0;
182 QStringIterator iter(target);
183 while (iter.hasNext()) {
184 qsizetype head = iter.index();
185 if (QChar::isSpace(iter.next())) {
186 qsizetype same = head > 0 ? matchFront(view, target.first(head)) : 0;
189 QStringIterator viter(view, same);
191 if (!viter.hasNext() || !QChar::isSpace(viter.next()))
193 same = spaceForward(viter);
195 view = view.sliced(same);
196 target = target.sliced(spaceForward(iter));
197 iter = QStringIterator(target);
201 const qsizetype tail = target.isEmpty() ? 0 : matchFront(view, target);
204 return matched + tail;
208 qsizetype offset = 0;
210 QStringView view = text.sliced(from + offset);
211 if (flags.testFlag(F::IgnoreCase)) {
212 if (qsizetype match = beginLength(view, sought, Qt::CaseInsensitive))
213 return {from, from + offset + match};
214 }
else if (flags.testAnyFlags(F::LowerCase | F::UpperCase)) {
217 if (flags.testFlag(F::LowerCase)) {
218 if (qsizetype match = beginLength(view, sought.toLower()))
219 return {from, from + offset + match};
221 if (flags.testFlag(F::UpperCase)) {
222 if (qsizetype match = beginLength(view, sought.toUpper()))
223 return {from, from + offset + match};
226 }
else if (qsizetype match = beginLength(view, sought)) {
227 return {from, from + offset + match};
231 if (!allowLeadingSpace) {
237 QStringIterator iter(text.sliced(from), offset);
238 if (!iter.hasNext() || !QChar::isSpace(iter.next()))
241 Q_ASSERT(iter.index() > offset);
242 offset = iter.index();
243 }
while (text.size() >= offset + sought.size() / 2);
252 return left.results.endIndex > right.results.endIndex;
256 qxp::function_ref<
void(QLocale::FormatType)
const> action)
258 using Flag = TemporalFieldFlag;
259 constexpr auto Widths = FieldGroup::WidthMask;
260 if (matchesFlagWithin(flags, Flag::Wide, Widths))
261 action(QLocale::LongFormat);
262 if (matchesFlagsWithin(flags, Flag::Short | Flag::Abbreviated, Widths))
263 action(QLocale::ShortFormat);
264 if (matchesFlagWithin(flags, Flag::Narrow, Widths))
265 action(QLocale::NarrowFormat);
270 const QLocale locale;
271 const QCalendar calendar;
272 const std::optional<
int> baseYear;
284 qsizetype maxDigits = 0;
286 qsizetype roundAfter = -1;
287 bool allowSign =
false;
290 static int &millisTarget(
PartialParse &grow) {
return grow.results.millis; }
291 static int &secondTarget(
PartialParse &grow) {
return grow.results.second; }
292 static int &minuteTarget(
PartialParse &grow) {
return grow.results.minute; }
293 static int &hourTarget(
PartialParse &grow) {
return grow.results.hour; }
295 static int &dayOfWeekTarget(
PartialParse &grow) {
return grow.results.dayOfWeek; }
296 static int &dayOfMonthTarget(
PartialParse &grow) {
return grow.results.dayOfMonth; }
297 static int &monthTarget(
PartialParse &grow) {
return grow.results.month; }
300 if (!grow.results.year)
301 grow.results.year = 0;
302 return *grow.results.year;
306 std::vector<PartialParse>
307 numericExtend(
const PartialParse &base, QStringView text,
308 TemporalFieldFlags flags, FieldConfig &&config)
const;
311 std::vector<PartialParse> monthNameExtend(
const PartialParse &base, QStringView text,
312 TemporalFieldFlags flags)
const;
313 std::vector<PartialParse> dayNameExtend(
const PartialParse &base, QStringView text,
314 TemporalFieldFlags flags)
const;
315 std::pair<qsizetype,
int> dayPeriodPrefix(
const PartialParse &base, QStringView text,
316 TemporalFieldFlags flags)
const;
323 const TemporalField &field)
const;
329 TemporalFieldCategory category)
const
334 using Cat = TemporalFieldCategory;
335 if (category == Cat::Literal)
338 const bool newYear = category == Cat::Year || category == Cat::YearWithinCentury;
344 const bool newDate = (newYear || category == Cat::Month || category == Cat::DayOfMonth
345 || category == Cat::DayOfWeek);
346 if (newDate && parse.results.month && parse.results.dayOfMonth) {
348 if (parse.results.year) {
349 if (!calendar.isDateValid(*parse.results.year, parse.results.month,
350 parse.results.dayOfMonth)) {
353 if (parse.results.dayOfWeek) {
354 QDate date = calendar.dateFromParts(*parse.results.year, parse.results.month,
355 parse.results.dayOfMonth);
356 if (calendar.dayOfWeek(date) != parse.results.dayOfWeek)
359 }
else if (calendar.daysInMonth(parse.results.month) < parse.results.dayOfMonth) {
364 if ((category == Cat::PeriodInDay && parse.results.hour >= 0)
365 || (category == Cat::Hour && parse.periodInDay >= 0)) {
367 if (parse
.periodInDay ? parse.results.hour < 12 : parse.results.hour >= 12)
371 if ((category == Cat::Hour && parse.hourMod12 > 0)
372 || (category == Cat::HourMod12 && parse.results.hour >= 0)) {
373 if ((parse.results.hour - parse
.hourMod12) % 12)
386 if (parse.results.year) {
389 }
else if (baseYear) {
390 const auto baseSplit =QRoundingDown::qDivMod<100>(*baseYear);
395 if (parse.results.month) {
398 const auto enough = [dom = parse.results.dayOfMonth](
int dim) {
399 return dim > 0 && (!dom || dom <= dim);
401 if (!enough(calendar.daysInMonth(parse.results.month, year))) {
404 for (
int off = 1; off < 10; ++off) {
405 int offset = off * 100;
406 if (enough(calendar.daysInMonth(parse.results.month, year + offset))) {
411 if (enough(calendar.daysInMonth(parse.results.month, year - offset))) {
422 if (parse.results.dayOfMonth) {
423 if (parse.results.dayOfWeek) {
424 QCalendar::YearMonthDay ymd
425 = { year, parse.results.month, parse.results.dayOfMonth };
427 = calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
428 if (!resolved.isValid())
430 year = resolved.year(calendar);
432 const QDate resolved(year, parse.results.month, parse.results.dayOfMonth);
433 if (!resolved.isValid())
439 parse.results.year = year;
443 if (parse.results.hour < 0 && parse
.hourMod12 > 0) {
447 parse.results.hour += 12;
450 if (parse.results.year && parse.results.month && parse.results.dayOfMonth
451 && parse.results.zone.isValid() && parse.results.hour >= 0) {
453 const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
455 Q_ASSERT(date.isValid());
456 const QTime time = parse.results.time(QTime());
457 Q_ASSERT(time.isValid());
460 if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
461 QDateTime::TransitionResolution::Reject).isValid())) {
464 QDateTime dt(date, time, parse.results.zone, parse.results.resolveType());
467 dt = QDateTime(date, time, parse.results.zone);
471 if (dt.date() != date || dt.time() != time
472 || dt.timeRepresentation() != parse.results.zone) {
486 if (parse.results.hour < 0) {
496std::vector<PartialParse>
498 TemporalFieldFlags flags, FieldConfig &&config)
const
500 std::vector<PartialParse> matches;
502 using Flag = TemporalFieldFlag;
503 qsizetype leadingSpace = 0;
504 const bool spacePad = flags.testFlag(Flag::SpacePad);
506 QStringIterator iter(text, base.results.endIndex);
507 while (iter.hasNext() && QChar::isSpace(iter.next()))
512 const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
513 locale, config.allowSign);
514 const bool zeroPad = flags.testFlag(Flag::ZeroPad);
516 const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
519 QByteArrayView digits{parsed.digits};
523 constexpr int intMaxDigits = std::numeric_limits<
int>::digits10 + 1;
524 if (digits.size() > intMaxDigits)
525 digits = digits.first(intMaxDigits);
527 if (config.maxDigits > 0) {
529 const int maxWidth = qMax(config.maxDigits, config.width);
530 if (digits.size() > maxWidth)
531 digits = digits.first(maxWidth);
532 }
else if (flags.testFlag(Flag::YearSignIso8601) && !parsed.sign) {
534 const int maxWidth = qMax(-config.maxDigits, config.width > 0 ? config.width : 1);
535 if (digits.size() > maxWidth)
536 digits = digits.first(maxWidth);
540 const qsizetype appendThreshold = config.maxDigits <= 0
541 ? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
544 for (; digits.size() >= width; digits.chop(1)) {
546 unsigned whole = digits.toUInt(&ok);
549 if (config.maxValue > 0 && config.roundAfter < 0 && whole >
unsigned(config.maxValue))
553 bool forbidZero = config.unset == 0 && (config.width < 4 || !calendar.hasYearZero());
554 auto optvalue = [whole, forbidZero,
555 negate = parsed.sign ==
'-']() ->
std::optional<
int> {
556 constexpr unsigned maxInt = std::numeric_limits<
int>::max();
557 if (negate && whole == 1 + maxInt)
558 return std::numeric_limits<
int>::min();
559 if (whole > maxInt || (forbidZero && !whole))
561 return negate ? -
int(whole) :
int(whole);
565 int value = *optvalue;
567 if (config.roundAfter >= 0) {
569 if (digits.size() < config.roundAfter) {
571 for (
int i =
int(digits.size()); i < config.roundAfter; ++i)
573 }
else if (digits.size() > config.roundAfter) {
575 for (
int i =
int(digits.size()); i > config.roundAfter; --i)
581 value = v > config.maxValue ? config.maxValue : qRound(v);
590 int &target = config.target(grow);
591 if (target <= config.unset)
593 else if (target != value)
595 grow.results.endIndex = parsed.digitStart + digits.size() * parsed.digitWidth;
597 if (!zeroPad && digits.size() > qMax(1, config.width)
598 && (config.roundAfter < 0 ? digits.startsWith(
'0') : digits.endsWith(
'0'))) {
601 if (digits.size() + leadingSpace < config.width)
607 if (digits.size() > appendThreshold)
608 matches.insert(matches.begin(),
std::move(grow));
610 matches.push_back(
std::move(grow));
616
617
618
619
620
621
622
623
624
625
626
627
628std::vector<PartialParse>
630 TemporalFieldFlags flags)
const
632 std::vector<PartialParse> matches;
633 using Flag = TemporalFieldFlag;
635 const auto addIfMatch = [&matches, &base, text, flags](
int month,
const QString &name) {
637 Q_ASSERT(!base.results.month || base.results.month == month);
641 auto match = matchesAt(text, base.results.endIndex, name, flags);
643 matches.emplace_back(base, match).results.month = month;
644 if (flags.testFlag(Flag::SpacePad))
645 matches = spacePadExtend(std::move(matches), text);
649 constexpr auto Forms = FieldGroup::FormMask;
650 constexpr int noYear = QCalendar::Unspecified;
651 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
652 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
653 const int year = base.results.year ? *base.results.year : noYear;
663 const auto tryEachNameType = [&](QLocale::FormatType form,
int month) {
665 addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
667 addIfMatch(month, calendar.monthName(locale, month, year, form));
676 const auto tryEachMonth = [month = base.results.month,
677 bound = calendar.maximumMonthsInYear(),
678 &tryEachNameType](QLocale::FormatType form) {
680 tryEachNameType(form, month);
682 for (
int i = bound; i > 0; --i)
683 tryEachNameType(form, i);
686 forEachLocaleFormat(flags, tryEachMonth);
691std::vector<PartialParse>
693 TemporalFieldFlags flags)
const
695 std::vector<PartialParse> matches;
696 using Flag = TemporalFieldFlag;
698 const auto addIfMatch = [&matches, &base, text, flags](
int dow,
const QString &name) {
700 Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
704 auto match = matchesAt(text, base.results.endIndex, name, flags);
706 matches.emplace_back(base, match).results.dayOfWeek = dow;
707 if (flags.testFlag(Flag::SpacePad))
708 matches = spacePadExtend(std::move(matches), text);
712 constexpr auto Forms = FieldGroup::FormMask;
713 const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
714 const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
715 const auto tryEachNameType = [&](QLocale::FormatType form,
int dow) {
717 addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
719 addIfMatch(dow, calendar.weekDayName(locale, dow, form));
725 const auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
726 &tryEachNameType](QLocale::FormatType form) {
728 tryEachNameType(form, dow);
733 for (
int i = 1; i <= 7; ++i)
734 tryEachNameType(form, i);
737 forEachLocaleFormat(flags, tryEachDayOfWeek);
743std::pair<qsizetype,
int>
745 TemporalFieldFlags flags)
const
747 std::pair<qsizetype,
int> result = {0, -1};
748 for (
int i = 0; i < 2; ++i) {
751 if (
const QString token = i ? locale.pmText() : locale.amText(); !token.isEmpty()) {
752 if (
auto match = matchesAt(text, base.results.endIndex, token, flags);
753 match.endIndex > result.first) {
754 result = { match.endIndex, i };
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
799 const TemporalField &field)
const
801 std::vector<PartialParse> matches;
802 const qsizetype textPos = base.results.endIndex;
803 switch (field.category) {
804 using Cat = TemporalFieldCategory;
805 using Flag = TemporalFieldFlag;
807 if (
auto match = matchesAt(text, textPos, field.literal, field.options)) {
808 matches.emplace_back(base, match);
809 if (field.options.testFlag(Flag::SpacePad))
810 matches = spacePadExtend(std::move(matches), text);
814 if (
const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
816 for (
const auto &match : zones) {
817 matches.emplace_back(base, match);
818 if (field.options.testFlag(Flag::SpacePad))
819 matches = spacePadExtend(std::move(matches), text);
825 case Cat::SecondFraction:
826 matches = numericExtend(base, text, field.options,
827 {millisTarget, 999, -1, field.width, 0, 3});
830 matches = numericExtend(base, text, field.options, {secondTarget, 59, -1, field.width, 2});
834 matches = numericExtend(base, text, field.options, {minuteTarget, 59, -1, field.width, 2});
838 matches = numericExtend(base, text, field.options,
839 {hourMod12Target, 12, 0, field.width, 2});
842 matches = numericExtend(base, text, field.options, {hourTarget, 23, -1, field.width, 2});
844 case Cat::PeriodInDay:
845 if (
const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
849 grow.results.endIndex = match.first;
851 if (field.options.testFlag(Flag::SpacePad))
852 matches = spacePadExtend(std::move(matches), text);
857 matches = dayNameExtend(base, text, field.options);
859 case Cat::DayOfMonth: {
860 const int maxDays = calendar.maximumDaysInMonth();
861 matches = numericExtend(base, text, field.options,
862 {dayOfMonthTarget, maxDays, 0, field.width,
863 maxDays < 10 ? 1 : maxDays < 100 ? 2 : 3});
872 matches = monthNameExtend(base, text, field.options);
873 if (matchesFlagWithin(field.options, Flag::Numeric, FieldGroup::FormMask)) {
874 auto extend = numericExtend(base, text, field.options,
875 {monthTarget, calendar.maximumMonthsInYear(),
878 matches =
std::move(extend);
880 matches.insert(matches.end(), extend.begin(), extend.end());
885 case Cat::YearWithinCentury:
886 matches = numericExtend(base, text, field.options,
887 {yearWithinCenturyTarget, 99, -1, field.width, 2});
890 matches = numericExtend(base, text, field.options,
891 {yearTarget, 0, 0, field.width, -4, -1, calendar.isProleptic()});
1043 const QLocale &locale, QCalendar cal,
1044 std::optional<
int> baseYear, qsizetype from)
1046 if (from < 0 || from >= text.size())
1051 std::vector<PartialParse> maybe;
1052 maybe.emplace_back(from);
1054 qsizetype toCome = fields.size();
1055 for (
const QtTemporalPattern::TemporalField &field : fields) {
1057 const std::vector<PartialParse> prior = std::exchange(maybe, {});
1058 for (
const PartialParse &base : prior) {
1059 std::vector<PartialParse> more
1060 = matcher.continuations(base, text, field);
1061 for (PartialParse &candidate : more) {
1063 if ((field.category == TemporalFieldCategory::Literal
1064 || matcher.isSelfConsistent(candidate, field.category))) {
1066 candidate.results.bounds.push_back(candidate.results.endIndex);
1067 else if (!matcher.resolve(candidate))
1069 maybe.push_back(std::move(candidate));
1086 for (
const PartialParse &match : QSpan{maybe}.sliced(1)) {
1087 if (match.compare(best) < 0)
1090 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)
void forEachLocaleFormat(TemporalFieldFlags flags, qxp::function_ref< void(QLocale::FormatType) const > action)
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)