Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "private/qtparsetemporal_p.h"
#include "private/qcalendarmath_p.h"
#include "private/qlocale_p.h"
#include "private/qstringiterator_p.h"
#include "private/qttemporalpattern_p.h"
#include <algorithm> // sort, stable_sort
#include <QtCore/qxpfunctional.h>
#include <optional>
#include <utility> // exchange, move, pair
#include <vector>
QT_BEGIN_NAMESPACE
namespace {
using namespace QtParseTemporal;
using namespace QtTemporalPattern;
struct PartialParse
{
ParsedTemporal results;
int periodInDay = -1; // 0: am, 1: pm
int hourMod12 = 0; // 1 through 12
int yearWithinCentury = -1; // 0 through 99
static constexpr int UnknownAmHour = -12, UnknownPmHour = 36;
enum Flaw : quint16 {
// Flaws that justify prefering a shorter parse without the flaw over
// longer with it, in order of decreasing severity:
Irreconcilable = 1, // field values cannot be reconciled
// Ideally continuations() would catch irreconcilable issues, but if one
// is expensive to spot it can be left for resolve() to flag up.
LegacyResolves = 2, // field values can only be resolved by ignoring timeType
ResolutionChanges = 4, // resolved values don't match parsed values
ZeroPad = 8, // used zero-padded part of text where field didn't require it
Narrow = 0x10, // used fewer digits from text than field width, where allowed
// Flaws not worth giving up a longer parse over, in decreasing order of
// strength of preference among those of equal length:
SelfResolved = 0x100, // ambiguous field values resolve cleanly
};
Q_DECLARE_FLAGS(Flaws, Flaw)
Flaws wanton = {};
// Constructor for initial empty parse:
PartialParse(qsizetype from) { results.startIndex = results.endIndex = from; }
// Constructors extending a parse with something more:
PartialParse(const PartialParse &base, const QtParseCommon::ParsedText &more)
: PartialParse(base)
{
Q_ASSERT(results.endIndex == more.startIndex);
results.endIndex = more.endIndex;
}
PartialParse(const PartialParse &base, const QtParseTimeZone::ParsedZone &more)
: PartialParse(base, (const QtParseCommon::ParsedText &)more)
{
results.zone = more.zone;
results.timeType = more.timeType;
}
Qt::weak_ordering compare(const PartialParse &alt) const noexcept
{
// Measures of how this->wanton differs from alt.wanton: it's worse if
// it has a flaw that alt lacks, better if the opposite. Here, "less" is
// used to mean "this is better than alt" as we sort better entries
// earlier in our lists of partial parse candidates.
const auto order = [better = alt.wanton & ~wanton,
worse = wanton & ~alt.wanton](Flaw test) {
Q_ASSERT(!(worse & better)); // So at most one of these testFlag()s is true:
if (better.testFlag(test))
return Qt::weak_ordering::less;
if (worse.testFlag(test))
return Qt::weak_ordering::greater;
return Qt::weak_ordering::equivalent;
};
if (auto res = order(Flaw::Irreconcilable); res != 0)
return res;
// In decreasing order of severity
if (auto res = order(Flaw::LegacyResolves); res != 0)
return res;
if (auto res = order(Flaw::ResolutionChanges); res != 0)
return res;
if (auto res = order(Flaw::Narrow); res != 0)
return res;
// The above take precedence over size: a shorter parse without those
// flaws is better than a longer one with them.
// Longer is better, to be understood as "sorts before" i.e. less than.
if (auto res = Qt::compareThreeWay(alt.results.size(), results.size()); res != 0)
return res;
// The following remains as a preference only among matches of the same
// length: it's nice to avoid, but a longer parse is still better.
if (auto res = order(Flaw::ZeroPad); res != 0)
return res;
if (auto res = order(Flaw::SelfResolved); res != 0)
return res;
return Qt::weak_ordering::equivalent;
}
};
Q_DECLARE_OPERATORS_FOR_FLAGS(PartialParse::Flaws)
QLocaleData::DigitSequence
parseDigitSequence(QStringView text, qsizetype from, const QLocale &locale, bool allowSign)
{
const auto *const data = QLocalePrivate::get(locale)->m_data;
using DS = QLocaleData::DigitSequence;
DS::Options flags;
if (allowSign)
flags.setFlag(DS::Option::AllowSign, true);
return data->digitSequence(text, flags, from);
}
std::vector<PartialParse> spacePadExtend(std::vector<PartialParse> matched, QStringView text)
{
// Pass the whole text: the results.endIndex of the last entry in matched is
// an index into it that we shall use to add entries that extend that entry.
Q_ASSERT(!matched.empty());
// Assumes matched.back()'s last field is allowed to end in space-padding
// and inserts a partial parse resulting from accepting each subsequent
// space as extending the match. Each longer match is inserted before all
// shorter matches. Only extensions of matched.back() are added, so call
// after adding each entry to matched, if adding several.
const qsizetype position = matched.size() - 1;
PartialParse copy = matched.back();
QStringIterator iter(text, copy.results.endIndex);
while (iter.hasNext() && QChar::isSpace(iter.next())) {
Q_ASSERT(iter.index() > copy.results.endIndex);
copy.results.endIndex = iter.index();
matched.insert(matched.begin() + position, copy);
}
return matched;
}
QtParseCommon::ParsedText matchedAt(QStringView text, qsizetype from, const QString &sought,
TemporalFieldFlags flags)
{
using F = TemporalFieldFlag;
const bool allowLeadingSpace = flags.testFlag(F::SpacePad);
Q_ASSERT(sought.size() > 0);
// Note: returns the first match within text[from:]. If sought is all space
// and SpacePad is set, there may be later matches if text[from:] starts
// with more space than (possibly some non-matching spaces, then) that.
// caller is expected to follow the match implied by the return from this
// with more generated by spacePadExtend().
const auto beginLength = [flex = flags.testFlag(F::FlexSpace),
strict = flags.testFlag(F::StrictSpace)]
(QStringView view, QStringView target, Qt::CaseSensitivity cs = Qt::CaseSensitive) {
// Technical hitch: case-insensitive comparison may match a string of
// different length. Roll a brute-force length-determining version:
const auto matchFront = [cs](QStringView view, QStringView target) -> qsizetype {
if (target.isEmpty()) // Only empty matches empty.
return 0;
if (view.startsWith(target, cs)) {
qsizetype length = target.size();
// Non-empty target can't be matched by view.first(0) so length
// == 1 won't satisfy this loop condition and, as length is
// initially > 0, we don't need to bounds-check this:
while (view.first(length - 1).startsWith(target, cs))
--length;
// We know view as a whole does start with target, so this won't
// run past length == view.size():
while (!view.first(length).startsWith(target, cs))
++length;
Q_ASSERT(length > 0);
return length;
}
return -1;
};
const auto spacingForward = [](QStringIterator &iter) {
// Steps iter past next non-space, returns index at which it appeared.
qsizetype used;
do {
used = iter.index();
} while (iter.hasNext() && QChar::isSpace(iter.next()));
return used;
};
const auto matchingSpace = [](char32_t need, char32_t have) {
constexpr char32_t ASCIImax = 0x7f;
// Treat SPACE and non-ASCII spaces as equivalent:
if (need == U' ' || need > ASCIImax)
return have == U' ' || have > ASCIImax;
// Otherwise, require exact match:
return need == have;
};
constexpr qsizetype failed = 0;
qsizetype matched = 0;
if (flex || !strict) {
QStringIterator iter(target);
// Advance through the string; on each space, check match since last
// space then matching of spaces; and prune view, target past match.
while (iter.hasNext()) {
qsizetype head = iter.index();
if (const char32_t tgt = iter.next(); QChar::isSpace(tgt)) {
// Match any non-space stepped over since start or last space:
qsizetype same = head ? matchFront(view, target.first(head)) : 0;
if (same < 0)
return failed;
QStringIterator viter(view, same);
// Require at least one spacing character in view to match target's:
if (!viter.hasNext())
return failed;
if (const char32_t got = viter.next(); !QChar::isSpace(got))
return failed;
else if (flex)
same = spacingForward(viter);
else if (!matchingSpace(tgt, got))
return failed;
else
same = viter.index();
matched += same;
view = view.sliced(same);
target = target.sliced(flex ? spacingForward(iter) : iter.index());
iter = QStringIterator(target);
// In the flex case, the next iteration will step over a non-space:
Q_ASSERT(!flex || !iter.hasNext() || !QChar::isSpace(iter.peekNext()));
}
}
// If target isn't empty, it's a spacing-free tail, to be checked
// the same way we would the whole would be in strict mode.
}
const qsizetype tail = matchFront(view, target);
if (tail < 0)
return failed;
return matched + tail;
};
// TODO: consider a comparison that ignores Unicode invisibles, like BiDi
// markers, when matching.
qsizetype offset = 0;
do {
QStringView view = text.sliced(from + offset);
if (flags.testFlag(F::IgnoreCase)) {
if (qsizetype match = beginLength(view, sought, Qt::CaseInsensitive))
return {from, from + offset + match};
} else if (flags.testAnyFlags(F::LowerCase | F::UpperCase)) {
// If either case is specified, only match specified cases.
// If both cases are specified, accept either (but not mixed).
if (flags.testFlag(F::LowerCase)) {
if (qsizetype match = beginLength(view, sought.toLower()))
return {from, from + offset + match};
}
if (flags.testFlag(F::UpperCase)) {
if (qsizetype match = beginLength(view, sought.toUpper()))
return {from, from + offset + match};
}
// Otherwise, only an exact match is accepted:
} else if (qsizetype match = beginLength(view, sought)) {
return {from, from + offset + match};
}
// No match at this position; maybe later if leading space is allowed:
if (!allowLeadingSpace) {
Q_ASSERT(!offset);
break;
}
// Consume one space at a time until we find a match:
QStringIterator iter(text.sliced(from), offset);
if (!iter.hasNext() || !QChar::isSpace(iter.next()))
break;
Q_ASSERT(iter.index() > offset);
offset = iter.index();
} while (text.size() >= offset + sought.size() / 2);
// Loop wants to test text.size() >= offset + sought.size(), but see beginLength().
return {};
}
bool longerEarlier(const PartialParse &left, const PartialParse &right)
{
// True if we want left before right in our sorted lists.
// We want longer matches before shorter:
return left.results.endIndex > right.results.endIndex;
}
void forEachLocaleFormat(TemporalFieldFlags flags,
qxp::function_ref<void(QLocale::FormatType) const> action)
{
using Flag = TemporalFieldFlag;
constexpr auto Widths = FieldGroup::WidthMask;
if (matchesFlagWithin(flags, Flag::Wide, Widths))
action(QLocale::LongFormat);
if (matchesFlagsWithin(flags, Flag::Short | Flag::Abbreviated, Widths))
action(QLocale::ShortFormat);
if (matchesFlagWithin(flags, Flag::Narrow, Widths))
action(QLocale::NarrowFormat);
}
class TemporalFieldMatcher
{
const QLocale locale;
const QCalendar calendar;
const std::optional<int> baseYear;
// Numeric
struct FieldConfig
{
// Where to write the int, once read:
int &(*target)(PartialParse &);
bool alreadySet = false; // Did base parse already know this field ?
// Acceptable values:
int maxValue = 0; // 0 means unbounded
bool allowZero = false;
// Form of the parsed text:
qsizetype width; // min digits
qsizetype maxDigits = 0; // <= 0 means unbounded
// If unbounded, beyond max(width, roundAfter, -maxDigits) prefer fewer digits to more.
qsizetype roundAfter = -1; // >= 0: is fractional part: round to this many digits
// (Values with fewer than roundAfter digits will also be *=10'd up to match.)
// Special cases for year:
bool allowSign = false;
bool allowNegative = false;
};
// For use as FieldConfig::target:
static int &millisTarget(PartialParse &grow) { return grow.results.millis; }
static int &secondTarget(PartialParse &grow) { return grow.results.second; }
static int &minuteTarget(PartialParse &grow) { return grow.results.minute; }
static int &hourTarget(PartialParse &grow) { return grow.results.hour; }
static int &hourMod12Target(PartialParse &grow) { return grow.hourMod12; }
static int &dayOfWeekTarget(PartialParse &grow) { return grow.results.dayOfWeek; }
static int &dayOfMonthTarget(PartialParse &grow) { return grow.results.dayOfMonth; }
static int &monthTarget(PartialParse &grow) { return grow.results.month; }
static int &yearTarget(PartialParse &grow)
{
// Always called on a freshly-copied grow that shall be discarded unless
// year was previously unset or equal to the value about to be set. In
// the latter case we already have that value to return a reference to
// and in the former the caller already knows to overwrite, so setting
// any value will suffice to give us a reference it can overwrite:
if (!grow.results.year)
grow.results.year = 0;
return *grow.results.year;
}
static int &yearWithinCenturyTarget(PartialParse &grow) { return grow.yearWithinCentury; }
std::vector<PartialParse>
numericExtend(const PartialParse &base, QStringView text,
TemporalFieldFlags flags, FieldConfig &&config) const;
// Verbal, Standalone:
std::vector<PartialParse> monthNameExtend(const PartialParse &base, QStringView text,
TemporalFieldFlags flags) const;
std::vector<PartialParse> dayNameExtend(const PartialParse &base, QStringView text,
TemporalFieldFlags flags) const;
std::pair<qsizetype, int> dayPeriodPrefix(const PartialParse &base, QStringView text,
TemporalFieldFlags flags) const;
public:
TemporalFieldMatcher(const QLocale &loc, QCalendar cal, std::optional<int> centuryStart)
: locale(loc), calendar(cal), baseYear(centuryStart)
{}
std::vector<PartialParse> continuations(const PartialParse &base, QStringView text,
const TemporalField &field) const;
bool isSelfConsistent(const PartialParse &parsed, TemporalFieldCategory category) const;
bool resolve(PartialParse &parsed) const;
};
bool TemporalFieldMatcher::isSelfConsistent(const PartialParse &parse,
TemporalFieldCategory category) const
{
// Take into account calendar, and potentially baseYear, but only do cheap
// checks. This will be run on *each* candidate parse after *each* field,
// need not check conditions the current field could not have affected.
using Cat = TemporalFieldCategory;
if (category == Cat::Literal) // Can't have introduced any inconsistency.
return true;
const bool newYear = category == Cat::Year || category == Cat::YearWithinCentury;
if (newYear && parse.yearWithinCentury >= 0 && parse.results.year
&& (*parse.results.year - parse.yearWithinCentury) % 100) {
return false;
}
const bool newDate = (newYear || category == Cat::Month || category == Cat::DayOfMonth
|| category == Cat::DayOfWeek);
if (newDate && parse.results.month && parse.results.dayOfMonth) {
// Calendrical calculations: somewhat expensive, but still arithmetic.
if (parse.results.year) {
if (!calendar.isDateValid(*parse.results.year, parse.results.month,
parse.results.dayOfMonth)) {
return false;
}
if (parse.results.dayOfWeek) {
QDate date = calendar.dateFromParts(*parse.results.year, parse.results.month,
parse.results.dayOfMonth);
if (calendar.dayOfWeek(date) != parse.results.dayOfWeek)
return false;
}
} else if (calendar.daysInMonth(parse.results.month) < parse.results.dayOfMonth) {
return false;
}
}
if ((category == Cat::PeriodInDay && parse.results.hour >= 0)
|| (category == Cat::Hour && parse.periodInDay >= 0)) {
// 00, 01, ... 11 are 12, 1, ... 11 am; 12, 13, ... 23 are 12, 1, ..., 11 pm.
if (parse.periodInDay ? parse.results.hour < 12 : parse.results.hour >= 12)
return false;
}
if ((category == Cat::Hour && parse.hourMod12 > 0)
|| (category == Cat::HourMod12 && parse.results.hour >= 0)) {
if ((parse.results.hour - parse.hourMod12) % 12)
return false;
}
return true;
}
bool TemporalFieldMatcher::resolve(PartialParse &parse) const
{
// Final pass, modifying parsed as needed, true if parse.result has been
// given a value consistent with all fields of parse. Applies fully rigorous
// checks, given what isSelfConsistent() already checked. May record flaws
// in parse.wanton where relevant tests reveal them.
if (parse.yearWithinCentury >= 0) {
if (parse.results.year) {
// Previously checked by isSelfConsistent():
Q_ASSERT((*parse.results.year - parse.yearWithinCentury) % 100 == 0);
} else if (baseYear) {
const auto baseSplit =QRoundingDown::qDivMod<100>(*baseYear);
int year = baseSplit.quotient * 100 + parse.yearWithinCentury;
if (parse.yearWithinCentury < baseSplit.remainder)
year += 100;
if (parse.results.month) {
// Check the year has this month and, if given, enough days in
// it for dayOfMonth:
const auto enough = [dom = parse.results.dayOfMonth](int dim) {
return dim > 0 && (!dom || dom <= dim);
};
if (!enough(calendar.daysInMonth(parse.results.month, year))) {
// Search outwards for a better century:
bool fixed = false;
for (int off = 1; off < 10; ++off) {
int offset = off * 100;
if (enough(calendar.daysInMonth(parse.results.month, year + offset))) {
year += offset;
fixed = true;
break;
}
if (enough(calendar.daysInMonth(parse.results.month, year - offset))) {
year -= offset;
fixed = true;
break;
}
}
// No century within a millennium each way will do:
if (!fixed)
return false;
}
if (parse.results.dayOfMonth) {
if (parse.results.dayOfWeek) {
QCalendar::YearMonthDay ymd
= { year, parse.results.month, parse.results.dayOfMonth };
const QDate resolved
= calendar.matchCenturyToWeekday(ymd, parse.results.dayOfWeek);
if (!resolved.isValid())
return false;
year = resolved.year(calendar);
} else {
const QDate resolved(year, parse.results.month, parse.results.dayOfMonth);
if (!resolved.isValid())
return false;
}
}
}
parse.results.year = year;
}
}
if (parse.results.hour < 0 && parse.hourMod12 > 0) {
Q_ASSERT(parse.hourMod12 <= 12);
parse.results.hour = parse.hourMod12 < 12 || parse.periodInDay < 0 ? parse.hourMod12 : 0;
if (parse.periodInDay > 0)
parse.results.hour += 12;
}
if (parse.results.year && parse.results.month && parse.results.dayOfMonth
&& parse.results.zone.isValid() && parse.results.hour >= 0) {
// Should be able to construct a datetime with this:
const QDate date(*parse.results.year, parse.results.month, parse.results.dayOfMonth,
calendar);
Q_ASSERT(date.isValid()); // Should be ensured by earlier checks.
const QTime time = parse.results.time(QTime());
Q_ASSERT(time.isValid()); // Should be ensured by earlier checks.
// Is the given time in a transition of the given zone, on the given date ?
if (!Q_LIKELY(QDateTime(date, time, parse.results.zone,
QDateTime::TransitionResolution::Reject).isValid())) {
// Ambiguity, gap or outright borkage.
using Flaw = PartialParse::Flaw;
QDateTime dt(date, time, parse.results.zone, parse.results.resolveType());
if (!dt.isValid()) {
// Fall back to default resolution (same as LegacyBehavior):
dt = QDateTime(date, time, parse.results.zone);
// If that succeeded, Abbreviated (bad); otherwise Narrow (worse).
parse.wanton |= dt.isValid() ? Flaw::LegacyResolves : Flaw::Irreconcilable;
}
if (dt.date() != date || dt.time() != time
|| dt.timeRepresentation() != parse.results.zone) {
// OK, resolution *worked* but didn't get exactly what we asked
// for (presumably a spring-forward's gap):
parse.wanton |= Flaw::ResolutionChanges;
// ... but we don't change parse.results because they should
// reflect what parsing learned; the caller can rediscover this.
} else {
// We got what we asked for (presumably the expected branch of a
// fall-back):
parse.wanton |= Flaw::SelfResolved;
}
}
}
if (parse.results.hour < 0) {
// Leave ParsedTemporal::time() a clue to am/pm, if known:
if (parse.periodInDay > 0)
parse.results.hour = PartialParse::UnknownPmHour;
else if (parse.periodInDay == 0)
parse.results.hour = PartialParse::UnknownAmHour;
}
return true;
}
std::vector<PartialParse>
TemporalFieldMatcher::numericExtend(const PartialParse &base, QStringView text,
TemporalFieldFlags flags, FieldConfig &&config) const
{
std::vector<PartialParse> matches;
using Flag = TemporalFieldFlag;
qsizetype leadingSpace = 0;
const bool spacePad = flags.testFlag(Flag::SpacePad);
if (spacePad) {
QStringIterator iter(text, base.results.endIndex);
while (iter.hasNext() && QChar::isSpace(iter.next()))
++leadingSpace;
// If that's used up the string, the code below shall reject the field.
}
const auto parsed = parseDigitSequence(text, base.results.endIndex + leadingSpace,
locale, config.allowSign);
if (parsed.sign == '-' && !config.allowNegative)
return matches;
const bool zeroPad = flags.testFlag(Flag::ZeroPad);
// If !zeroPad, we allow < config.width but flag with Narrow in wanton fields.
const int width = zeroPad || spacePad ? qMax(1, config.width - leadingSpace) : 1;
// This is necessarily positive: the use of chop(1) below depends on that.
QByteArrayView digits{parsed.digits};
// Parsed field must be representable in an int, so don't try to read more
// digits than an int can hold (digits10 is how many 9s in a row an int can
// hold; but int can hold some sequences one digit longer than that):
constexpr int intMaxDigits = std::numeric_limits<int>::digits10 + 1;
if (digits.size() > intMaxDigits)
digits = digits.first(intMaxDigits);
// Take config and flags into account, too:
if (config.maxDigits > 0) {
// Allow config.width to override config.maxDigits:
const int maxWidth = qMax(config.maxDigits, config.width);
if (digits.size() > maxWidth)
digits = digits.first(maxWidth);
} else if (flags.testFlag(Flag::YearSignIso8601) && !parsed.sign) {
// Limit width because a field longer than width would need a sign.
const int maxWidth = qMax(-config.maxDigits, config.width > 0 ? config.width : 1);
if (digits.size() > maxWidth)
digits = digits.first(maxWidth);
}
// For unbounded, work out in advance when to switch from prepending to
// appending; otherwise, set a cut-off that'll be true already.
const qsizetype appendThreshold = config.maxDigits <= 0
? qMax(-config.maxDigits, qMax(config.width, config.roundAfter)) - 1
: digits.size();
for (; digits.size() >= width; digits.chop(1)) {
bool ok = false;
unsigned whole = digits.toUInt(&ok);
if (!ok)
continue;
if (config.maxValue > 0 && config.roundAfter < 0 && whole > unsigned(config.maxValue))
continue;
auto optvalue = [whole, forbidZero = !config.allowZero,
negate = parsed.sign == '-']() -> std::optional<int> {
constexpr unsigned maxInt = std::numeric_limits<int>::max();
if (negate && whole == 1 + maxInt)
return std::numeric_limits<int>::min();
if (whole > maxInt || (forbidZero && !whole))
return {};
return negate ? -int(whole) : int(whole);
}();
if (!optvalue) // Overflow or too low
continue;
int value = *optvalue;
if (config.roundAfter >= 0) {
// Fractional part
if (digits.size() < config.roundAfter) {
// Interpolate omitted zero-padding up to rounding size:
for (int i = int(digits.size()); i < config.roundAfter; ++i)
value *= 10;
} else if (digits.size() > config.roundAfter) {
double v = value;
for (int i = int(digits.size()); i > config.roundAfter; --i)
v /= 10.;
// A timestamp that's before the end of a specified second
// should be rounded to the last we can before that second,
// especially if it's the last second of its minute, in turn
// especially if that's the last second of its hour (and so on).
value = v > config.maxValue ? config.maxValue : qRound(v);
// There may of course be use-cases where rounding up to the
// next second is desired. If it turns out those are
// significant, we can perhaps add a field option for it.
}
// else: exact match to number of digits, nothing to frob.
}
PartialParse grow = base;
int &target = config.target(grow);
if (!config.alreadySet) // If not previously set, store:
target = value;
else if (target != value) // Conflicts with earlier field: skip this reading.
continue;
grow.results.endIndex = parsed.digitBounds[digits.size()];
if (!zeroPad && digits.size() > qMax(1, config.width)
&& (config.roundAfter < 0 ? digits.startsWith('0') : digits.endsWith('0'))) {
grow.wanton |= PartialParse::Flaw::ZeroPad;
}
if (digits.size() + leadingSpace < config.width) // (can only happen if !zeroPad)
grow.wanton |= PartialParse::Flaw::Narrow;
// Entries in matches are all longer than this one, as we're reducing
// digits. Mostly we want shorter after longer, but (for example) we
// prefer 4-digit years over longer matches.
if (digits.size() > appendThreshold)
matches.insert(matches.begin(), std::move(grow));
else
matches.push_back(std::move(grow));
}
return matches;
}
/* Some month names may be prefixes of others.
For example, the English long forms of Islamic calendar month names include:
* RabiÊ» I, RabiÊ» II
* Jumada I, Jumada II
Their short-forms are likewise:
* Rab. I, Rab. II
* Jum. I, Jum. II
In each case, one month name is a prefix of the next month's name.
In any sane format, greedy parsing shall suffice but ill-considered formats
happen. So the initial parse recognizes every possible match and we sort out
any mistakes greed might make as we parse later fields.
*/
std::vector<PartialParse>
TemporalFieldMatcher::monthNameExtend(const PartialParse &base, QStringView text,
TemporalFieldFlags flags) const
{
std::vector<PartialParse> matches;
using Flag = TemporalFieldFlag;
const auto addIfMatch = [&matches, &base, text, flags](int month, const QString &name) {
// tryEachMonth() has ensured this:
Q_ASSERT(!base.results.month || base.results.month == month);
if (name.isEmpty()) // Locale doesn't know this month's name.
return;
// If matchedAt(), add to matches:
auto match = matchedAt(text, base.results.endIndex, name, flags);
if (match) {
matches.emplace_back(base, match).results.month = month;
if (flags.testFlag(Flag::SpacePad))
matches = spacePadExtend(std::move(matches), text);
}
};
constexpr auto Forms = FieldGroup::FormMask;
constexpr int noYear = QCalendar::Unspecified;
const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
const int year = base.results.year ? *base.results.year : noYear;
// We could try to take account of baseYear, when yearWithinCentury is
// known, but that's susceptible to tweaks and perturbation from other
// fields, so stick with noYear and the usual naming of months if we don't
// know year. We can consider adding a QCalendar::parseMonthName() that can
// consult the internal lists of localized month names, both for efficiency
// and to ensure we try all names, including those that appear only in some
// years. If we do that, its return should package month number, whether the
// month appears in all years and whether it was standalone or plain, along
// with the start and end indices of the match within the text.
const auto tryEachNameType = [&](QLocale::FormatType form, int month) {
if (lone)
addIfMatch(month, calendar.standaloneMonthName(locale, month, year, form));
if (verb)
addIfMatch(month, calendar.monthName(locale, month, year, form));
};
// This could in principle, for non-system locales, be done more efficiently
// by walking the internal ';'-joined list of month names QCalendarBackend
// can give us. The entanglement between QCalendarBackend and QLocale
// internals is, however, already quite untidy enough, so leave that for
// if/when we discover it's a significant bottle-neck and/or we've unpicked
// the existing entanglement a bit first.
const auto tryEachMonth = [month = base.results.month,
bound = calendar.maximumMonthsInYear(),
&tryEachNameType](QLocale::FormatType form) {
if (month > 0) {
tryEachNameType(form, month);
} else {
for (int i = bound; i > 0; --i)
tryEachNameType(form, i);
}
};
forEachLocaleFormat(flags, tryEachMonth);
return matches;
}
std::vector<PartialParse>
TemporalFieldMatcher::dayNameExtend(const PartialParse &base, QStringView text,
TemporalFieldFlags flags) const
{
std::vector<PartialParse> matches;
using Flag = TemporalFieldFlag;
const auto addIfMatch = [&matches, &base, text, flags](int dow, const QString &name) {
// tryEachDayOfWeek() has ensured this:
Q_ASSERT(!base.results.dayOfWeek || base.results.dayOfWeek == dow);
if (name.isEmpty()) // Locale doesn't know this day of the week's name.
return;
// If matchedAt(), add to matches:
auto match = matchedAt(text, base.results.endIndex, name, flags);
if (match) {
matches.emplace_back(base, match).results.dayOfWeek = dow;
if (flags.testFlag(Flag::SpacePad))
matches = spacePadExtend(std::move(matches), text);
}
};
constexpr auto Forms = FieldGroup::FormMask;
const bool verb = matchesFlagWithin(flags, Flag::Verbal, Forms);
const bool lone = matchesFlagWithin(flags, Flag::Standalone, Forms);
const auto tryEachNameType = [&](QLocale::FormatType form, int dow) {
if (lone)
addIfMatch(dow, calendar.standaloneWeekDayName(locale, dow, form));
if (verb)
addIfMatch(dow, calendar.weekDayName(locale, dow, form));
};
// As for month names (see above), some collaboration with QCalendarBackend
// might make this more efficient for non-system locales, at the expense of
// adding to the existing tangle of complexity.
const auto tryEachDayOfWeek = [dow = base.results.dayOfWeek,
&tryEachNameType](QLocale::FormatType form) {
if (dow > 0) {
tryEachNameType(form, dow);
} else {
// Iterate possible day numbers. Issue: some calendars might have
// intercalary days with numbers > 7. When that happens, we may
// need to let this run past 7 until it's seen some empty answers.
for (int i = 1; i <= 7; ++i)
tryEachNameType(form, i);
}
};
forEachLocaleFormat(flags, tryEachDayOfWeek);
std::sort(matches.begin(), matches.end(), longerEarlier);
return matches;
}
std::pair<qsizetype, int>
TemporalFieldMatcher::dayPeriodPrefix(const PartialParse &base, QStringView text,
TemporalFieldFlags flags) const
{
std::pair<qsizetype, int> result = {0, -1};
for (int i = 0; i < 2; ++i) {
if (base.periodInDay >= 0 && base.periodInDay != i)
continue;
if (const QString token = i ? locale.pmText() : locale.amText(); !token.isEmpty()) {
if (auto match = matchedAt(text, base.results.endIndex, token, flags);
match.endIndex > result.first) {
result = { match.endIndex, i };
}
}
}
return result;
}
/*!
\internal
Find all matches to \a field, within \a text, that extend \a base.
Each match must begin at offset \c{base.results.endIndex} within \a text.
For each match, update a copy of \a base with the match's result, to include
in the returned list.
May use \c calendar to determine the range of values allowed for field.
Does not attempt to determine consistency between fields; see resolve() and
isSelfConsistent() for that. Updates the copy's member holding the value
described by \a field to reflect the match.
Ignores base.result.startIndex and base.result.bounds and updates each
copy's .endIndex to reflect the end of the match. (This leaves the caller to
decide whether to transfer that to .bounds.)
For fields that allow space padding, this consumes leading space as
necessary to make a match and includes a match for each end position at
which it could end; before any dangling space and after each space that
follows. Later calls to \c continuations() shall filter out any earlier
matches that precludes later fields matching just after its end. Successive
space-padded fields surrounded by large amounts of space are apt to lead to
many matches, as are final space-padded fields followed by large amounts of
space. (TODO: we can almost certainly mitigate this with a trivial
heuristic, once everything is working.)
For those matches with various flaws, relative to the field specification
(such as using zero padding when not obliged to), the copy's .wanton records
that flaw.
Sort order of the returned list should put entries likely to represent more
suitable matches (ignoring .wanton complications) earlier. For most fields,
that means longer matches come first. (For full year field matches with > 4
digits, though, that reverses.)
*/
std::vector<PartialParse>
TemporalFieldMatcher::continuations(const PartialParse &base, QStringView text,
const TemporalField &field) const
{
std::vector<PartialParse> matches;
const qsizetype textPos = base.results.endIndex;
switch (field.category) {
using Cat = TemporalFieldCategory;
using Flag = TemporalFieldFlag;
case Cat::EndCategories:
Q_UNREACHABLE();
case Cat::Literal:
if (auto match = matchedAt(text, textPos, field.literal, field.options)) {
matches.emplace_back(base, match);
if (field.options.testFlag(Flag::SpacePad))
matches = spacePadExtend(std::move(matches), text);
}
break;
case Cat::TimeZone:
if (const auto zones = QtParseTimeZone::prefix(text, locale, textPos, field.options);
!zones.isEmpty()) {
for (const auto &match : zones) {
matches.emplace_back(base, match);
if (field.options.testFlag(Flag::SpacePad))
matches = spacePadExtend(std::move(matches), text);
}
}
break;
// case Cat::MillisecondInDay: break;
case Cat::SecondFraction:
// QTime, QDateTime only support millisecond precision, so use 3 as roundAfter.
// That's also maxDigits, unless RoundFraction => unlimited.
matches = numericExtend(base, text, field.options,
{millisTarget, base.results.millis >= 0, 999, true, field.width,
field.options.testFlag(Flag::RoundFraction) ? 0 : 3, 3});
break;
case Cat::Second:
matches = numericExtend(base, text, field.options,
{secondTarget, base.results.second >= 0, 59, true, field.width, 2});
break;
// case Cat::MinuteFraction: break;
case Cat::Minute:
matches = numericExtend(base, text, field.options,
{minuteTarget, base.results.minute >= 0, 59, true, field.width, 2});
break;
// case Cat::HourFraction: break;
case Cat::HourMod12:
matches = numericExtend(base, text, field.options,
{hourMod12Target, base.hourMod12 > 0, 12, false, field.width, 2});
break;
case Cat::Hour:
matches = numericExtend(base, text, field.options,
{hourTarget, base.results.hour >= 0, 23, true, field.width, 2});
break;
case Cat::PeriodInDay: // am/pm; LDML also has noon, midnight, "at night" and others.
if (const auto match = dayPeriodPrefix(base, text, field.options); match.second >= 0) {
// Ensured by dayPeriodPrefix:
Q_ASSERT(base.periodInDay < 0 || base.periodInDay == match.second);
PartialParse &grow = matches.emplace_back(base);
grow.results.endIndex = match.first;
grow.periodInDay = match.second;
if (field.options.testFlag(Flag::SpacePad))
matches = spacePadExtend(std::move(matches), text);
}
break;
case Cat::DayOfWeek:
matches = dayNameExtend(base, text, field.options);
break;
case Cat::DayOfMonth: {
const int maxDays = calendar.maximumDaysInMonth();
matches = numericExtend(base, text, field.options,
{dayOfMonthTarget, base.results.dayOfMonth > 0, maxDays, false,
field.width, maxDays < 10 ? 1 : maxDays < 100 ? 2 : 3});
}
break;
// case Cat::DayOfYear: break;
// case Cat::JulianDay: break;
// case Cat::WeekOfMonth: break;
// case Cat::WeekOfYear: break;
case Cat::Month:
// Verbal and Standalone, in so far as supported:
matches = monthNameExtend(base, text, field.options);
if (matchesFlagWithin(field.options, Flag::Numeric, FieldGroup::FormMask)) {
auto extend = numericExtend(base, text, field.options,
{monthTarget, base.results.month > 0,
calendar.maximumMonthsInYear(), false, field.width, 2});
if (matches.empty())
matches = std::move(extend);
else
matches.insert(matches.end(), extend.begin(), extend.end());
}
std::sort(matches.begin(), matches.end(), longerEarlier);
break;
// case Cat::Quarter: break;
case Cat::YearWithinCentury:
matches = numericExtend(base, text, field.options,
{yearWithinCenturyTarget, base.yearWithinCentury >= 0,
99, true, field.width, 2});
break;
case Cat::Year:
matches = numericExtend(base, text, field.options,
{yearTarget, bool(base.results.year), 0, calendar.hasYearZero(),
field.width, -4, -1, true, calendar.isProleptic()});
break;
// case Cat::RelatedGregorianYear: break;
// case Cat::Century: break;
// case Cat::Era: break;
}
return matches;
}
} // nameless namespace
namespace QtParseTemporal {
QDate ParsedTemporal::date(QCalendar cal, QDate defaults) const
{
if (defaults.isValid()) {
QDate draft(year ? *year : defaults.year(cal), month ? month : defaults.month(cal),
dayOfMonth ? dayOfMonth : defaults.day(cal), cal);
if (dayOfWeek && draft.isValid() && draft.dayOfWeek(cal) != dayOfWeek) {
// Defaults conflict with parsed day of the week.
if (!dayOfMonth) {
// (Assumes no intercalary days.)
// Number of days to the nearest with the right day of the week:
const int offset = (dayOfWeek + 10 - draft.dayOfWeek(cal)) % 7 - 3;
Q_ASSERT(offset != 0); // Otherwise, day of week matched, already.
Q_ASSERT(-4 < offset && offset < 4);
// Prefer closer unless nearby has more in common with what we asked for:
QDate closer = draft.addDays(offset);
QDate nearby = draft.addDays(offset < 0 ? offset + 7 : offset - 7);
if (nearby.isValid()
&& (!closer.isValid()
|| (closer.dayOfWeek(cal) != dayOfWeek
&& nearby.dayOfWeek(cal) == dayOfWeek)
|| (closer.month(cal) != draft.month(cal)
&& nearby.month(cal) == draft.month(cal)))) {
// (We could also give year(cal) the same treatment, but
// different year, for dates within ten days of one another,
// plies different month, so check would be redundant.)
std::swap(nearby, draft);
} else if (closer.isValid() && closer.dayOfWeek(cal) == dayOfWeek) {
std::swap(closer, draft);
}
} else if (!month) {
Q_ASSERT(draft.day() == dayOfMonth);
auto use = [&draft, cal, dow=dayOfWeek](int yr, int mon, int day) {
QDate maybe(yr, mon, day, cal);
if (!maybe.isValid() || maybe.dayOfWeek(cal) != dow)
return false;
std::swap(maybe, draft);
return true;
};
// Find nearest month with the right dayOfMonth and dayOfWeek.
// If year was specified we're limited to it; otherwise,
// draft.year() is derived from defaults so the search can
// spread to nearby years.
int loYear = draft.year(cal), hiYear = loYear;
int loMon = draft.month(cal), hiMon = loMon;
bool maybeLo = true, maybeHi = true;
while (maybeLo || maybeHi) {
if (maybeHi) {
if (hiMon < cal.monthsInYear(hiYear)) {
++hiMon;
} else if (year) {
Q_ASSERT(hiYear == *year);
maybeHi = false;
} else if (hiYear + 1 || cal.hasYearZero()) {
++hiYear;
hiMon = 1;
} else if (cal.isProleptic()) {
hiYear = +1;
hiMon = 1;
} else {
maybeHi = false;
}
}
if (maybeHi && use(hiYear, hiMon, dayOfMonth))
break;
if (maybeLo) {
if (loMon > 1) {
--loMon;
} else if (year) {
Q_ASSERT(loYear == *year);
maybeLo = false;
} else if (loYear - 1 || cal.hasYearZero()) {
--loYear;
loMon = cal.monthsInYear(loYear);
} else if (cal.isProleptic()) {
loYear = -1;
loMon = cal.monthsInYear(loYear);
} else {
maybeLo = false;
}
}
if (maybeLo && use(loYear, loMon, dayOfMonth))
break;
// Avoid looping for ever: if we can't find a match within a
// 30 year window we probably never shall. If we haven't
// found a match by then, the likelihood that the input has
// a typo in it is fairly high, in any case.
if (hiYear - loYear > 30)
break;
}
} else if (!year) {
// As for resolve()'s handling of two-digit centuries:
QCalendar::YearMonthDay ymd = { draft.year(cal), month, dayOfMonth };
QDate maybe = cal.matchCenturyToWeekday(ymd, dayOfWeek);
if (maybe.isValid() && maybe.dayOfWeek(cal) == dayOfWeek)
std::swap(maybe, draft);
}
if (draft.dayOfWeek(cal) != dayOfWeek)
return {};
}
return draft;
}
if (year && month && dayOfMonth)
return QDate(*year, month, dayOfMonth, cal);
return {};
}
QTime ParsedTemporal::time(QTime defaults) const
{
if (defaults.isValid()) {
int hr = defaults.hour();
// hour: -1 means we have no information, less means unknown am, > 24 means unknown pm.
if (hour < -1) // UnknownAmHour
hr = hr % 12;
else if (hour > 24) // UnknownPmHour
hr = hr % 12 + 12;
else if (hour >= 0)
hr = hour;
// (Note: hour == 24 is currently unused but may be relevant for 24:00:00 in future.)
return QTime(hr,
minute < 0 ? defaults.minute() : minute,
second < 0 ? defaults.second() : second,
millis < 0 ? defaults.msec() : millis);
}
if (hour < 0 || hour > 24)
return {};
if (minute < 0)
return QTime(hour, 0);
if (second < 0)
return QTime(hour, minute);
if (millis < 0)
return QTime(hour, minute, second);
return QTime(hour, minute, second, millis);
}
ParsedTemporal prefix(QStringView text, QSpan<const QtTemporalPattern::TemporalField> fields,
const QLocale &locale, QCalendar cal,
std::optional<int> baseYear, qsizetype from)
{
if (from < 0 || from >= text.size())
return {};
const TemporalFieldMatcher matcher(locale, cal, baseYear);
// Technically this is the correct (empty) result when fields.isEmpty():
std::vector<PartialParse> maybe;
maybe.emplace_back(from);
qsizetype toCome = fields.size();
for (const QtTemporalPattern::TemporalField &field : fields) {
--toCome;
const std::vector<PartialParse> prior = std::exchange(maybe, {});
for (const PartialParse &base : prior) {
std::vector<PartialParse> more
= matcher.continuations(base, text, field);
for (PartialParse &candidate : more) {
// Consistency won't have been changed by a literal field:
if ((field.category == TemporalFieldCategory::Literal
|| matcher.isSelfConsistent(candidate, field.category))) {
if (toCome) // Earlier fields' ends go in bounds:
candidate.results.bounds.push_back(candidate.results.endIndex);
else if (!matcher.resolve(candidate)) // Last field: makes sense of it all.
continue;
maybe.push_back(std::move(candidate));
}
}
}
if (maybe.empty()) // No point continuing
return {};
}
// Now select our most favourable entry from maybe.
// Although we've, thus far, prefered sensible-length matches over longer
// ones in individual numeric fields, so that later numeric fields can take
// up the slack and win, we still want to be greedy over-all, so prefer
// overall longer matches to shorter ones. None the less, between matches of
// equal length, preserve our preference, up to now, for sane lengths of
// each field within that, as long as later fields are taking up the slack.
// That preference can be fine-tuned via .wanton, see PartialParse::Flaw.
PartialParse best = maybe.front();
for (const PartialParse &match : QSpan{maybe}.sliced(1)) {
if (match.compare(best) < 0)
best = match;
}
return best.results;
}
} // QtParseTemporal
QT_END_NAMESPACE