Alkimia API

alkdateformat.cpp
1/*
2 SPDX-FileCopyrightText: 2004 Ace Jones acejones @users.sourceforge.net
3 SPDX-FileCopyrightText: 2018-2019 Thomas Baumgart tbaumgart @kde.org
4
5 This file is part of libalkimia.
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include "alkdateformat.h"
11
12#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
13#include <KCalendarSystem>
14#include <KGlobal>
15#include <QRegExp>
16#else
17#include <QLocale>
18#include <QRegularExpression>
19#include <QRegularExpressionMatch>
20#endif
21
22class AlkDateFormat::Private
23{
24public:
25 QString m_format;
26 AlkDateFormat::ErrorCode m_errorCode;
27 QString m_errorMessage;
28
29 QDate setError(AlkDateFormat::ErrorCode errorCode, const QString& arg1 = QString(), const QString& arg2 = QString())
30 {
31 m_errorCode = errorCode;
32 switch(errorCode) {
33 case AlkDateFormat::NoError:
34 m_errorMessage.clear();
35 break;
36 case AlkDateFormat::InvalidFormatString:
37 m_errorMessage = QString("Invalid format string '%1'").arg(arg1);
38 break;
39 case AlkDateFormat::InvalidFormatCharacter:
40 m_errorMessage = QString("Invalid format character '%1'").arg(arg1);
41 break;
42 case AlkDateFormat::InvalidDate:
43 m_errorMessage = QString("Invalid date '%1'").arg(arg1);
44 break;
45 case AlkDateFormat::InvalidDay:
46 m_errorMessage = QString("Invalid day entry: %1").arg(arg1);
47 break;
48 case AlkDateFormat::InvalidMonth:
49 m_errorMessage = QString("Invalid month entry: %1").arg(arg1);
50 break;
51 case AlkDateFormat::InvalidYear:
52 m_errorMessage = QString("Invalid year entry: %1").arg(arg1);
53 break;
54 case AlkDateFormat::InvalidYearLength:
55 m_errorMessage = QString("Length of year (%1) does not match expected length (%2).").arg(arg1, arg2);
56 break;
57 }
58 return QDate();
59 }
60
61 QDate convertStringSkrooge(const QString &_in)
62 {
63 QDate date;
64 if (m_format == "UNIX") {
65 bool ok;
66 const quint64 unixTime = _in.toUInt(&ok);
67 if (ok) {
68 date = QDateTime::fromMSecsSinceEpoch(unixTime * 1000).date();
69 }
70
71 } else {
72 const QString skroogeFormat = m_format;
73
74 m_format = m_format.toLower();
75
76#if QT_VERSION < QT_VERSION_CHECK(5,0,0)
77 QRegExp formatrex("([mdy]+)(\\W+)([mdy]+)(\\W+)([mdy]+)", Qt::CaseInsensitive);
78 if (formatrex.indexIn(m_format) == -1) {
79 return setError(AlkDateFormat::InvalidFormatString, m_format);
80 }
81 m_format = QLatin1String("%");
82 m_format.append(formatrex.cap(1));
83 m_format.append(formatrex.cap(2));
84 m_format.append(QLatin1String("%"));
85 m_format.append(formatrex.cap(3));
86 m_format.append(formatrex.cap(4));
87 m_format.append(QLatin1String("%"));
88 m_format.append(formatrex.cap(5));
89#else
90 static QRegularExpression formatrex("([mdy]+)(\\W+)([mdy]+)(\\W+)([mdy]+)", QRegularExpression::CaseInsensitiveOption);
91 auto match = formatrex.match(m_format);
92 if (!match.hasMatch()) {
93 return setError(AlkDateFormat::InvalidFormatString, m_format);
94 }
95
96 m_format = QLatin1String("%");
97 m_format += match.captured(1);
98 m_format += match.captured(2);
99 m_format.append(QLatin1String("%"));
100 m_format += match.captured(3);
101 m_format += match.captured(4);
102 m_format.append(QLatin1String("%"));
103 m_format += match.captured(5);
104#endif
105 date = convertStringKMyMoney(_in, true, 2000);
106 m_format = skroogeFormat;
107 }
108 if (!date.isValid()) {
109 return setError(AlkDateFormat::InvalidDate, _in);
110 }
111 if (!m_format.contains(QStringLiteral("yyyy")) && date.year() < 2000)
112 date = date.addYears(100);
113 return date;
114 }
115
116 QDate convertStringUnix(const QString& _in)
117 {
118 bool ok;
119 quint64 unixTime = _in.toUInt(&ok);
120 if (!ok) {
121 return setError(AlkDateFormat::InvalidDate, _in);
122 }
123 if (m_format.startsWith(QLatin1String("%ud"))) {
124 unixTime *= 86400; // times seconds per day
125 }
126 return QDateTime::fromMSecsSinceEpoch(unixTime * 1000).date();
127 }
128
129#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
130
131 QDate convertStringKMyMoney(const QString &_in, bool _strict, unsigned _centurymidpoint)
132 {
133 if (m_format.startsWith(QLatin1String("%u"))) {
134 return convertStringUnix(_in);
135 }
136
137 //
138 // Break date format string into component parts
139 //
140
141 QRegExp formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", Qt::CaseInsensitive);
142 if (formatrex.indexIn(m_format) == -1) {
143 return setError(AlkDateFormat::InvalidFormatString, m_format);
144 }
145
146 QStringList formatParts;
147 formatParts += formatrex.cap(1);
148 formatParts += formatrex.cap(3);
149 formatParts += formatrex.cap(5);
150
151 QStringList formatDelimiters;
152 formatDelimiters += formatrex.cap(2);
153 formatDelimiters += formatrex.cap(4);
154
155 // make sure to escape delimiters that are special chars in regex
157 QRegExp specialChars("^[\\.\\\\\\?]$");
158 for(it = formatDelimiters.begin(); it != formatDelimiters.end(); ++it) {
159 if (specialChars.indexIn(*it) != -1)
160 (*it).prepend("\\");
161 }
162
163 //
164 // Break input string up into component parts,
165 // using the delimiters found in the format string
166 //
167
168 QRegExp inputrex;
169 inputrex.setCaseSensitivity(Qt::CaseInsensitive);
170
171 // strict mode means we must enforce the delimiters as specified in the
172 // format. non-strict allows any delimiters
173 if (_strict) {
174 inputrex.setPattern(QString("(\\w+)\\.?%1(\\w+)\\.?%2(\\w+)\\.?").arg(formatDelimiters[0],
175 formatDelimiters[1]));
176 } else {
177 inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
178 }
179
180 if (inputrex.indexIn(_in) == -1) {
181 return setError(AlkDateFormat::InvalidDate, _in);
182 }
183
184 QStringList scannedParts;
185 scannedParts += inputrex.cap(1).toLower();
186 scannedParts += inputrex.cap(2).toLower();
187 scannedParts += inputrex.cap(3).toLower();
188
189 //
190 // Convert the scanned parts into actual date components
191 //
192 unsigned day = 0, month = 0, year = 0;
193 bool ok;
194 QRegExp digitrex("(\\d+)");
195 QStringList::const_iterator it_scanned = scannedParts.constBegin();
196 QStringList::const_iterator it_format = formatParts.constBegin();
197 while (it_scanned != scannedParts.constEnd()) {
198 // decide upon the first character of the part
199 switch ((*it_format).at(0).cell()) {
200 case 'd':
201 // remove any extraneous non-digits (e.g. read "3rd" as 3)
202 ok = false;
203 if (digitrex.indexIn(*it_scanned) != -1) {
204 day = digitrex.cap(1).toUInt(&ok);
205 }
206 if (!ok || day > 31) {
207 return setError(AlkDateFormat::InvalidDay, *it_scanned);
208 }
209 break;
210 case 'm':
211 month = (*it_scanned).toUInt(&ok);
212 if (!ok) {
213 // maybe it's a textual date
214 unsigned i = 1;
215 while (i <= 12) {
216 if (KGlobal::locale()->calendar()->monthName(i, 2000).toLower() == *it_scanned
217 || KGlobal::locale()->calendar()->monthName(i, 2000,
218 KCalendarSystem::ShortName).
219 toLower() == *it_scanned) {
220 month = i;
221 }
222 ++i;
223 }
224 }
225
226 if (month < 1 || month > 12) {
227 return setError(AlkDateFormat::InvalidMonth, *it_scanned);
228 }
229
230 break;
231 case 'y':
232 if (_strict && (*it_scanned).length() != (*it_format).length()) {
233 return setError(AlkDateFormat::InvalidYearLength, *it_scanned, *it_format);
234 }
235
236 year = (*it_scanned).toUInt(&ok);
237
238 if (!ok) {
239 return setError(AlkDateFormat::InvalidYear, *it_scanned);
240 }
241
242 //
243 // 2-digit year case
244 //
245 // this algorithm will pick a year within +/- 50 years of the
246 // centurymidpoint parameter. i.e. if the midpoint is 2000,
247 // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
248 if (year < 100) {
249 unsigned centuryend = _centurymidpoint + 50;
250 unsigned centurybegin = _centurymidpoint - 50;
251
252 if (year < centuryend % 100) {
253 year += 100;
254 }
255 year += centurybegin - centurybegin % 100;
256 }
257
258 if (year < 1900) {
259 return setError(AlkDateFormat::InvalidYear, QString::number(year));
260 }
261
262 break;
263 default:
264 return setError(AlkDateFormat::InvalidFormatCharacter, QString((*it_format).at(0).cell()));
265 }
266
267 ++it_scanned;
268 ++it_format;
269 }
270 QDate result(year, month, day);
271 if (!result.isValid()) {
272 return setError(AlkDateFormat::InvalidDate, QString("yr:%1 mo:%2 dy:%3)").arg(year).arg(month).arg(day));
273 }
274
275 return result;
276 }
277
278#else // Qt5
279
280 QDate convertStringKMyMoney(const QString& _in, bool _strict, unsigned _centurymidpoint)
281 {
282 if (m_format.startsWith(QLatin1String("%u"))) {
283 return convertStringUnix(_in);
284 }
285
286 //
287 // Break date format string into component parts
288 //
289
290 QRegularExpression formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", QRegularExpression::CaseInsensitiveOption);
291 QRegularExpressionMatch match = formatrex.match(m_format);
292 if (!match.hasMatch()) {
293 return setError(AlkDateFormat::InvalidFormatString, m_format);
294 }
295
296 QStringList formatParts;
297 formatParts += match.captured(1);
298 formatParts += match.captured(3);
299 formatParts += match.captured(5);
300
301 QStringList formatDelimiters;
302 formatDelimiters += match.captured(2);
303 formatDelimiters += match.captured(4);
304
305 // make sure to escape delimiters that are special chars in regex
307 QRegularExpression specialChars("^[\\.\\\\\\?]$");
308 for(it = formatDelimiters.begin(); it != formatDelimiters.end(); ++it) {
309 QRegularExpressionMatch special = specialChars.match(*it);
310 if (special.hasMatch()) {
311 (*it).prepend("\\");
312 }
313 }
314
315 //
316 // Break input string up into component parts,
317 // using the delimiters found in the format string
318 //
319 QRegularExpression inputrex;
321
322 // strict mode means we must enforce the delimiters as specified in the
323 // format. non-strict allows any delimiters
324 if (_strict)
325 inputrex.setPattern(QString("(\\w+)\\.?%1(\\w+)\\.?%2(\\w+)\\.?").arg(formatDelimiters[0], formatDelimiters[1]));
326 else
327 inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
328
329 match = inputrex.match(_in);
330 if (!match.hasMatch()) {
331 return setError(AlkDateFormat::InvalidDate, _in);
332 }
333
334 QStringList scannedParts;
335 scannedParts += match.captured(1).toLower();
336 scannedParts += match.captured(2).toLower();
337 scannedParts += match.captured(3).toLower();
338
339 //
340 // Convert the scanned parts into actual date components
341 //
342 unsigned day = 0, month = 0, year = 0;
343 bool ok;
344 QRegularExpression digitrex("(\\d+)");
345 QStringList::const_iterator it_scanned = scannedParts.constBegin();
346 QStringList::const_iterator it_format = formatParts.constBegin();
347 while (it_scanned != scannedParts.constEnd()) {
348 // decide upon the first character of the part
349 switch ((*it_format).at(0).cell()) {
350 case 'd':
351 // remove any extraneous non-digits (e.g. read "3rd" as 3)
352 ok = false;
353 match = digitrex.match(*it_scanned);
354 if (match.hasMatch())
355 day = match.captured(1).toUInt(&ok);
356 if (!ok || day > 31)
357 return setError(AlkDateFormat::InvalidDay, *it_scanned);
358 break;
359 case 'm':
360 month = (*it_scanned).toUInt(&ok);
361 if (!ok) {
362 month = 0;
363 // maybe it's a textual date
364 unsigned i = 1;
365 // search the name in the current selected locale
366 QLocale locale;
367 while (i <= 12) {
368 if (locale.standaloneMonthName(i).toLower() == *it_scanned
369 || locale.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
370 month = i;
371 break;
372 }
373 ++i;
374 }
375 // in case we did not find the month in the current locale,
376 // we look for it in the C locale
377 if(month == 0) {
378 QLocale localeC(QLocale::C);
379 if( !(locale == localeC)) {
380 i = 1;
381 while (i <= 12) {
382 if (localeC.standaloneMonthName(i).toLower() == *it_scanned
383 || localeC.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
384 month = i;
385 break;
386 }
387 ++i;
388 }
389 }
390 }
391 }
392
393 if (month < 1 || month > 12)
394 return setError(AlkDateFormat::InvalidMonth, *it_scanned);
395
396 break;
397 case 'y':
398 if (_strict && (*it_scanned).length() != (*it_format).length())
399 return setError(AlkDateFormat::InvalidYearLength, *it_scanned, *it_format);
400
401 year = (*it_scanned).toUInt(&ok);
402
403 if (!ok)
404 return setError(AlkDateFormat::InvalidYear, *it_scanned);
405
406 //
407 // 2-digit year case
408 //
409 // this algorithm will pick a year within +/- 50 years of the
410 // centurymidpoint parameter. i.e. if the midpoint is 2000,
411 // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
412 if (year < 100) {
413 unsigned centuryend = _centurymidpoint + 50;
414 unsigned centurybegin = _centurymidpoint - 50;
415
416 if (year < centuryend % 100)
417 year += 100;
418 year += centurybegin - centurybegin % 100;
419 }
420
421 if (year < 1900)
422 return setError(AlkDateFormat::InvalidYear, QString::number(year));
423
424 break;
425 default:
426 return setError(AlkDateFormat::InvalidFormatCharacter, QString(QChar((*it_format).at(0).cell())));
427 }
428
429 ++it_scanned;
430 ++it_format;
431 }
432 QDate result(year, month, day);
433 if (! result.isValid())
434 return setError(AlkDateFormat::InvalidDate, QString("yr:%1 mo:%2 dy:%3)").arg(year).arg(month).arg(day));
435
436 return result;
437 }
438#endif
439
440};
441
442AlkDateFormat::AlkDateFormat(const QString &format)
443 : d(new Private)
444{
445 d->m_format = format;
446 d->m_errorCode = NoError;
447}
448
449AlkDateFormat::AlkDateFormat(const AlkDateFormat& right)
450 : d(new Private)
451{
452 d->m_format = right.d->m_format;
453 d->m_errorCode = NoError;
454}
455
456AlkDateFormat::~AlkDateFormat()
457{
458 delete d;
459}
460
461AlkDateFormat& AlkDateFormat::operator=(const AlkDateFormat& right)
462{
463 d->m_format = right.d->m_format;
464
465 return *this;
466}
467
468const QString & AlkDateFormat::format() const
469{
470 return d->m_format;
471}
472
473AlkDateFormat::ErrorCode AlkDateFormat::lastError() const
474{
475 return d->m_errorCode;
476}
477
478QString AlkDateFormat::lastErrorMessage() const
479{
480 return d->m_errorMessage;
481}
482
483QDate AlkDateFormat::convertString(const QString& date, bool strict, unsigned int centuryMidPoint)
484{
485 // reset any pending errors from previous runs
486 d->m_errorCode = NoError;
487 d->m_errorMessage.clear();
488
489 if (d->m_format.contains("%"))
490 return d->convertStringKMyMoney(date, strict, centuryMidPoint);
491 else
492 return d->convertStringSkrooge(date);
493}
494
495QString AlkDateFormat::convertDate(const QDate& date)
496{
497 Q_UNUSED(date);
498
499 // reset any pending errors from previous runs
500 d->m_errorCode = NoError;
501 d->m_errorMessage.clear();
502
503 return QString();
504}
Universal date converter.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT SimpleJob * special(const QUrl &url, const QByteArray &data, JobFlags flags=DefaultFlags)
QDate addYears(int nyears) const const
bool isValid(int year, int month, int day)
int year() const const
QDate date() const const
QDateTime fromMSecsSinceEpoch(qint64 msecs)
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator end()
QString standaloneMonthName(int month, FormatType type) const const
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
void setPattern(const QString &pattern)
void setPatternOptions(PatternOptions options)
QString & append(QChar ch)
QString arg(Args &&... args) const const
void clear()
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
uint toUInt(bool *ok, int base) const const
CaseInsensitive
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:01:13 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.