Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtparseqttemporalformat.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4#include "private/qtparseqttemporalformat_p.h"
5
6#include "private/qlocale_p.h"
7#include "private/qstringiterator_p.h"
8
9QT_BEGIN_NAMESPACE
10
11using namespace Qt::StringLiterals;
12
14
15inline constexpr char timeFormats[] = "Hhmsz"; // Omits [aA][pP]? deliberately.
16inline constexpr char dateFormats[] = "Mdy";
17
18/*!
19 \internal
20 Parse a prefix of \a pattern as a format for the data indicated by \a form.
21
22 Parses the longest prefix of \a pattern that constitutes a valid Qt temporal
23 format string. Only the field indicator characters for data indicated in \a
24 form are recognized as such; everything else (including what other forms
25 might accept as field indicators) is treated as literal text. The only text
26 that can be invalid is thus one containing an odd number of single quote
27 characters; the portion up to the last of these will be parsed, in that
28 case, and the return's \c endIndex will indicate the location of the
29 unmatched single quote. Otherwise, \c endIndex shall equal pattern.size().
30 The return's \c fields is a list of \l {QtTemporalPattern::} {TemporalField}
31 structures representing what was parsed, each with \c options matching the
32 pre-6.12 semantics of the Qt formats for temporal data, with some tweaks to
33 enable desired new features, notably years with (a leading sign and) more
34 than four digits.
35
36 \sa QtTemporalPattern, QDate::fromString(), QTime::fromString(), QDateTime::fromString()
37*/
38
39ParsedDateTimeFormat prefix(QStringView pattern, QtTemporalPattern::DateTimeParts form)
40{
41 using namespace QtTemporalPattern;
42
43 ParsedDateTimeFormat result;
44 constexpr char32_t Invalid = ~char32_t(0);
45 static_assert(Invalid > QChar::LastValidCodePoint);
46 const bool includeDate = form.testFlag(DateTimePart::Date);
47 const bool includeTime = form.testFlag(DateTimePart::Time);
48 const bool includeZone = form.testFlag(DateTimePart::Zone);
49
50 QStringIterator iter(pattern);
51 char32_t pending = 0;
52 const auto countRepeats = [&pending, &iter, &result](char32_t first, qsizetype bound) {
53 // Consumes all repeats of \a first, returns min(bound, number of repeats).
54 Q_ASSERT(!QChar::requiresSurrogates(first)); // It's always an ASCII format char
55 Q_ASSERT(pending == 0);
56 qsizetype count = 1; // We've already seen first
57 result.endIndex = iter.index(); // ... and tacitly consumed it.
58 while (iter.hasNext() && count < bound) {
59 const auto read = iter.next(Invalid);
60 if (read > QChar::LastValidCodePoint) {
61 pending = Invalid;
62 break;
63 }
64 if (read != first) {
65 pending = read;
66 break;
67 }
68 ++count;
69 result.endIndex = iter.index();
70 }
71 return count;
72 };
73
74 constexpr char32_t SingleQuote = U'\'';
75 static constexpr auto matchTimeFormats = QtPrivate::makeCharacterSetMatch<timeFormats>();
76 static constexpr auto matchDateFormats = QtPrivate::makeCharacterSetMatch<dateFormats>();
77 const auto isFormatChar = [includeDate, includeTime, includeZone](char32_t ch) {
78 if (ch >= 0x80)
79 return false;
80 if (includeTime) {
81 if (matchTimeFormats.matches(uchar(ch)) || ch == U'A' || ch == U'a')
82 return true;
83 }
84 if (includeZone && ch == U't')
85 return true;
86 return includeDate && matchDateFormats.matches(uchar(ch));
87 };
88
89 constexpr auto formatCategory = [](uchar ch, int count) {
90 using Cat = TemporalFieldCategory;
91 switch (ch) {
92 case 'A': // case 'P':
93 case 'a': // case 'p':
94 return Cat::PeriodInDay;
95 case 'd': return count < 3 ? Cat::DayOfMonth : Cat::DayOfWeek;
96 case 'H': return Cat::Hour;
97 case 'h': return Cat::HourMod12;
98 case 'M': return Cat::Month;
99 case 'm': return Cat::Minute;
100 case 's': return Cat::Second;
101 case 't': return Cat::TimeZone;
102 case 'y': return count < 3 ? Cat::YearWithinCentury : Cat::Year;
103 case 'z': return Cat::SecondFraction;
104 }
105 // Should only be called with a ch that would pass an isFormatChar() check.
106 Q_UNREACHABLE_RETURN(Cat::Literal);
107 };
108 constexpr auto formatFlags = [](uchar ch, int count) -> TemporalFieldFlags {
109 using F = TemporalFieldFlag;
110 constexpr TemporalFieldFlags TextCommon = F::IgnoreCase;
111 switch (ch) {
112 case 'A': // case 'P':
113 return count ? F::UpperCase | TextCommon : TextCommon;
114 case 'a': // case 'p':
115 return count ? F::LowerCase | TextCommon : TextCommon;
116 case 'd': case 'M': // Day and Month share a pattern:
117 switch (count) {
118 case 2: return F::Numeric | F::ZeroPad;
119 case 3: return F::Verbal | F::Abbreviated | TextCommon;
120 default:
121 return count < 2 ? F::Numeric : F::Verbal | F::Wide | TextCommon;
122 };
123 Q_UNREACHABLE();
124 break;
125 case 'H': case 'h': case 'm': case 's': // Shared pattern:
126 return count > 1 ? F::Numeric | F::ZeroPad : F::Numeric;
127 case 't':
128 switch (count) {
129 case 1: // 't': matches everything, serializes as abbreviation
130 return F::AllowZSuffix | F::LocalTimeName;
131 // The next two forms aren't localized - should they be ?
132 // The issue is that they're documented to differ in whether
133 // separators are used, but we don't control that (nor should
134 // we, or the format author) for localized forms.
135 case 2: // 'tt': offset (no-prefix, no separator)
136 return F::Iso8601 | F::Numeric | F::ZeroPad;
137 case 3: // 'ttt': offset (no-prefix, separator)
138 return F::Iso8601 | F::Verbal | F::ZeroPad;
139 default: // 'tttt': long name, IANA ID or LocalTime name
140 return F::LocalizedZone | F::Verbal | F::Standalone | F::Wide | F::Short
141 | F::LocalTimeName;
142 // This includes both metazone and exemplar city versions of long name.
143 }
144 Q_UNREACHABLE();
145 break;
146 case 'y':
147 if (count > 2)
148 return F::Numeric | F::ZeroPad | F::YearSignIso8601;
149 return F::Numeric | F::ZeroPad;
150 case 'z':
151 if (count > 2)
152 return F::Numeric | F::ZeroPad;
153 return F::Numeric;
154 }
155 // Should only be called by branches that passed an isFormatChar() check.
156 Q_UNREACHABLE_RETURN({});
157 };
158
159 const auto store = [&result](QString &&literal, qsizetype count,
160 TemporalFieldFlags flags,
161 TemporalFieldCategory category) {
162 result.fields.append(TemporalField{std::move(literal), count, flags, category});
163 };
164
165 bool seenDayPeriod = false, seenHourMod12 = false; // See post-processing.
166 while (pending <= QChar::LastValidCodePoint && (pending || iter.hasNext())) {
167 char32_t ch;
168 if (pending) {
169 ch = std::exchange(pending, 0);
170 } else {
171 result.endIndex = iter.index();
172 ch = iter.next(Invalid);
173 }
174 if (ch > QChar::LastValidCodePoint)
175 break;
176
177 if (ch < 0x80 && includeTime) {
178 if (matchTimeFormats.matches(uchar(ch))) {
179 qsizetype count = countRepeats(ch, ch == U'z' ? 3 : 2);
180 if (ch == U'z' && count == 2) // Backwards compatibility
181 count = 1; // (but we still consume both 'z' characters from the format)
182 store(QString(), count, formatFlags(uchar(ch), count),
183 formatCategory(uchar(ch), count));
184 if (ch == U'h')
185 seenHourMod12 = true;
186 continue;
187 }
188 if (ch == U'A' || ch == U'a') {
189 // Follow old QDTP (for now, at least) in using count to represent case choice.
190 qsizetype count = ch == U'a' ? 1 : 2;
191 // AP or ap are just the same as A or a; but Ap or aP selects
192 // locale-appropriate case:
193 result.endIndex = iter.index();
194 const auto read = iter.hasNext() ? iter.next(Invalid) : Invalid;
195 if (read > QChar::LastValidCodePoint) {
196 pending = Invalid;
197 } else if (read == U'P') {
198 if (ch == U'a')
199 count = 0;
200 result.endIndex = iter.index();
201 } else if (read == U'p') {
202 if (ch == U'A')
203 count = 0;
204 result.endIndex = iter.index();
205 } else {
206 pending = read;
207 }
208 store(QString(), count, formatFlags(uchar(ch), count),
209 formatCategory(uchar(ch), count));
210 seenDayPeriod = true;
211 continue;
212 }
213 }
214 // Date and Zone fields are more straightforward, except for 'y':
215 if (ch == U'y' && includeDate) {
216 // For 'y', a pair is a year-within-century, double that for a full
217 // year; beyond that, evenly many more are more of those but an odd
218 // 'y' is a literal. We thus need to only consume 2 or 4 'y' tokens,
219 // so can't use countRepeat() with its simple maximum. We need to
220 // leave the odd 'y', if present, for a later iteration to consume
221 // or, if it's all there is, for use as a literal - in which case we
222 // mustn't have set pending. Fortunately 'y' is ASCII so we don't
223 // have to worry about surrogates:
224 qsizetype count = 1;
225 QStringView tail = pattern.sliced(iter.index() - 1);
226 if (tail.size() > 4)
227 tail = tail.first(4);
228 while (count < tail.size() && char32_t(tail[count].unicode()) == ch)
229 ++count;
230 if (count == 3)
231 --count;
232 if (count > 1) {
233 Q_ASSERT(count == 2 || count == 4);
234 // Advance iter over what we've accepted:
235 iter.setPosition(iter.position() - 1 + count);
236 store(QString(), count, formatFlags(uchar(ch), count),
237 formatCategory(uchar(ch), count));
238 result.endIndex = iter.index();
239 continue;
240 }
241 // else: fall through to treat the lone 'y' as a literal.
242 } else if (ch < 0x80 && ((includeDate && matchDateFormats.matches(uchar(ch)))
243 || (includeZone && ch == U't'))) {
244 qsizetype count = countRepeats(ch, 4);
245 store(QString(), count, formatFlags(uchar(ch), count),
246 formatCategory(uchar(ch), count));
247 continue;
248 }
249 Q_ASSERT(pending == 0); // Everything that might set it has continue;d
250
251 // Not a field indicator, so parse as a literal:
252 QString literal;
253 QString quote; // If non-null: unfinished quote, to be appended to literal when closed.
254 if (ch == SingleQuote) { // Defer it to first iteration of loop below.
255 pending = ch;
256 } else {
257 literal = QString(QStringView(QChar::fromUcs4(ch)));
258 result.endIndex = iter.index();
259 }
260 while (pending <= QChar::LastValidCodePoint && (pending || iter.hasNext())) {
261 if (pending) {
262 ch = std::exchange(pending, 0);
263 } else {
264 if (quote.isNull()) // i.e. we're not in an incomplete quote
265 result.endIndex = iter.index();
266 ch = iter.next(Invalid);
267 }
268 if (ch > QChar::LastValidCodePoint)
269 break;
270
271 if (ch == SingleQuote) {
272 if (quote.isNull()) { // Provisionally start a quote
273 quote = u""_s; // empty is not null
274 } else {
275 // Even if this is the first quote of a pair, denoting a
276 // single quote within the quote, there's a valid parse that
277 // ends at it, adding the quote-so-far to literal.
278 literal += quote;
279 result.endIndex = iter.index();
280 quote = QString(); // Set back to null
281 }
282 ch = iter.hasNext() ? iter.next(Invalid) : Invalid;
283 if (ch == SingleQuote) {
284 // Paired single quote denotes a single quote:
285 if (quote.isNull()) {
286 // The quote we thought was ending actually continues:
287 // the continuation starts with a literal single quote.
288 quote = u"'"_s;
289 } else {
290 // Our provisionally-started quote was actually the
291 // first half of an pair of quotes not inside others.
292 Q_ASSERT(quote.isEmpty()); // We just set it.
293 quote = QString();
294 literal.append(u'\'');
295 result.endIndex = iter.index();
296 }
297 continue;
298 }
299
300 if (ch > QChar::LastValidCodePoint) {
301 pending = ch;
302 break;
303 }
304 }
305
306 Q_ASSERT(ch != SingleQuote);
307 if (quote.isNull() && isFormatChar(ch)) {
308 pending = ch;
309 break;
310 }
311 if (ch <= QChar::LastValidCodePoint) {
312 if (quote.isNull()) {
313 result.endIndex = iter.index();
314 literal.append(QStringView(QChar::fromUcs4(ch)));
315 } else {
316 quote.append(QStringView(QChar::fromUcs4(ch)));
317 }
318 }
319 }
320 // Even if we truncated due to an unclosed quote, we have a literal to
321 // include in the prefix we can parse as a pattern:
322 if (!literal.isEmpty())
323 store(std::move(literal), 0, {}, TemporalFieldCategory::Literal);
324
325 // If we're in an unclosed quote, we cleared pending or marked it invalid:
326 Q_ASSERT(quote.isNull() || !pending || pending > QChar::LastValidCodePoint);
327 }
328
329 // Post-process to deal with a quirk of the legacy format: if there's no
330 // AM/PM field, then 'h' format is read as 'H' format.
331 if (seenHourMod12 && !seenDayPeriod) {
332 for (TemporalField &field : result.fields) {
333 if (field.category == TemporalFieldCategory::HourMod12)
334 field.category = TemporalFieldCategory::Hour;
335 }
336 }
337
338 return result;
339}
340
341} // QtParseQtTemporalFormat
342
343QT_END_NAMESPACE
ParsedDateTimeFormat prefix(QStringView pattern, QtTemporalPattern::DateTimeParts form)