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