File indexing completed on 2025-02-09 05:59:17
0001 /* 0002 SPDX-FileCopyrightText: 2004 Ace Jones acejones @users.sourceforge.net 0003 SPDX-FileCopyrightText: 2018-2019 Thomas Baumgart tbaumgart @kde.org 0004 0005 This file is part of libalkimia. 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 0010 #include "alkdateformat.h" 0011 0012 #include <QDebug> 0013 0014 #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) 0015 #include <KGlobal> 0016 #include <KCalendarSystem> 0017 #include <QRegExp> 0018 #else 0019 #include <QLocale> 0020 #include <QRegularExpression> 0021 #include <QRegularExpressionMatch> 0022 #endif 0023 0024 0025 class AlkDateFormat::Private 0026 { 0027 public: 0028 QString m_format; 0029 AlkDateFormat::ErrorCode m_errorCode; 0030 QString m_errorMessage; 0031 0032 QDate setError(AlkDateFormat::ErrorCode errorCode, const QString& arg1 = QString(), const QString& arg2 = QString()) 0033 { 0034 m_errorCode = errorCode; 0035 switch(errorCode) { 0036 case AlkDateFormat::NoError: 0037 m_errorMessage.clear(); 0038 break; 0039 case AlkDateFormat::InvalidFormatString: 0040 m_errorMessage = QString("Invalid format string '%1'").arg(arg1); 0041 break; 0042 case AlkDateFormat::InvalidFormatCharacter: 0043 m_errorMessage = QString("Invalid format character '%1'").arg(arg1); 0044 break; 0045 case AlkDateFormat::InvalidDate: 0046 m_errorMessage = QString("Invalid date '%1'").arg(arg1); 0047 break; 0048 case AlkDateFormat::InvalidDay: 0049 m_errorMessage = QString("Invalid day entry: %1").arg(arg1); 0050 break; 0051 case AlkDateFormat::InvalidMonth: 0052 m_errorMessage = QString("Invalid month entry: %1").arg(arg1); 0053 break; 0054 case AlkDateFormat::InvalidYear: 0055 m_errorMessage = QString("Invalid year entry: %1").arg(arg1); 0056 break; 0057 case AlkDateFormat::InvalidYearLength: 0058 m_errorMessage = QString("Length of year (%1) does not match expected length (%2).").arg(arg1, arg2); 0059 break; 0060 } 0061 return QDate(); 0062 } 0063 0064 QDate convertStringSkrooge(const QString &_in) 0065 { 0066 QDate date; 0067 if (m_format == "UNIX") { 0068 #if QT_VERSION >= QT_VERSION_CHECK(5,8,0) 0069 date = QDateTime::fromSecsSinceEpoch(_in.toUInt(), Qt::UTC).date(); 0070 #else 0071 date = QDateTime::fromTime_t(_in.toUInt()).date(); 0072 #endif 0073 } else { 0074 const QString skroogeFormat = m_format; 0075 0076 m_format = m_format.toLower(); 0077 0078 #if QT_VERSION < QT_VERSION_CHECK(5,0,0) 0079 QRegExp formatrex("([mdy]+)(\\W+)([mdy]+)(\\W+)([mdy]+)", Qt::CaseInsensitive); 0080 if (formatrex.indexIn(m_format) == -1) { 0081 return setError(AlkDateFormat::InvalidFormatString, m_format); 0082 } 0083 m_format = QLatin1String("%"); 0084 m_format.append(formatrex.cap(1)); 0085 m_format.append(formatrex.cap(2)); 0086 m_format.append(QLatin1String("%")); 0087 m_format.append(formatrex.cap(3)); 0088 m_format.append(formatrex.cap(4)); 0089 m_format.append(QLatin1String("%")); 0090 m_format.append(formatrex.cap(5)); 0091 #else 0092 static QRegularExpression formatrex("([mdy]+)(\\W+)([mdy]+)(\\W+)([mdy]+)", QRegularExpression::CaseInsensitiveOption); 0093 auto match = formatrex.match(m_format); 0094 if (!match.hasMatch()) { 0095 return setError(AlkDateFormat::InvalidFormatString, m_format); 0096 } 0097 0098 m_format = QLatin1String("%"); 0099 m_format += match.captured(1); 0100 m_format += match.captured(2); 0101 m_format.append(QLatin1String("%")); 0102 m_format += match.captured(3); 0103 m_format += match.captured(4); 0104 m_format.append(QLatin1String("%")); 0105 m_format += match.captured(5); 0106 #endif 0107 date = convertStringKMyMoney(_in, true, 2000); 0108 m_format = skroogeFormat; 0109 } 0110 if (!date.isValid()) { 0111 return setError(AlkDateFormat::InvalidDate, _in); 0112 } 0113 if (!m_format.contains(QStringLiteral("yyyy")) && date.year() < 2000) 0114 date = date.addYears(100); 0115 return date; 0116 } 0117 0118 #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) 0119 0120 QDate convertStringKMyMoney(const QString &_in, bool _strict, unsigned _centurymidpoint) 0121 { 0122 // 0123 // Break date format string into component parts 0124 // 0125 0126 QRegExp formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", Qt::CaseInsensitive); 0127 if (formatrex.indexIn(m_format) == -1) { 0128 return setError(AlkDateFormat::InvalidFormatString, m_format); 0129 } 0130 0131 QStringList formatParts; 0132 formatParts += formatrex.cap(1); 0133 formatParts += formatrex.cap(3); 0134 formatParts += formatrex.cap(5); 0135 0136 QStringList formatDelimiters; 0137 formatDelimiters += formatrex.cap(2); 0138 formatDelimiters += formatrex.cap(4); 0139 0140 // make sure to escape delimiters that are special chars in regex 0141 QStringList::iterator it; 0142 QRegExp specialChars("^[\\.\\\\\\?]$"); 0143 for(it = formatDelimiters.begin(); it != formatDelimiters.end(); ++it) { 0144 if (specialChars.indexIn(*it) != -1) 0145 (*it).prepend("\\"); 0146 } 0147 0148 // 0149 // Break input string up into component parts, 0150 // using the delimiters found in the format string 0151 // 0152 0153 QRegExp inputrex; 0154 inputrex.setCaseSensitivity(Qt::CaseInsensitive); 0155 0156 // strict mode means we must enforce the delimiters as specified in the 0157 // format. non-strict allows any delimiters 0158 if (_strict) { 0159 inputrex.setPattern(QString("(\\w+)\\.?%1(\\w+)\\.?%2(\\w+)\\.?").arg(formatDelimiters[0], 0160 formatDelimiters[1])); 0161 } else { 0162 inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)"); 0163 } 0164 0165 if (inputrex.indexIn(_in) == -1) { 0166 return setError(AlkDateFormat::InvalidDate, _in); 0167 } 0168 0169 QStringList scannedParts; 0170 scannedParts += inputrex.cap(1).toLower(); 0171 scannedParts += inputrex.cap(2).toLower(); 0172 scannedParts += inputrex.cap(3).toLower(); 0173 0174 // 0175 // Convert the scanned parts into actual date components 0176 // 0177 unsigned day = 0, month = 0, year = 0; 0178 bool ok; 0179 QRegExp digitrex("(\\d+)"); 0180 QStringList::const_iterator it_scanned = scannedParts.constBegin(); 0181 QStringList::const_iterator it_format = formatParts.constBegin(); 0182 while (it_scanned != scannedParts.constEnd()) { 0183 // decide upon the first character of the part 0184 switch ((*it_format).at(0).cell()) { 0185 case 'd': 0186 // remove any extraneous non-digits (e.g. read "3rd" as 3) 0187 ok = false; 0188 if (digitrex.indexIn(*it_scanned) != -1) { 0189 day = digitrex.cap(1).toUInt(&ok); 0190 } 0191 if (!ok || day > 31) { 0192 return setError(AlkDateFormat::InvalidDay, *it_scanned); 0193 } 0194 break; 0195 case 'm': 0196 month = (*it_scanned).toUInt(&ok); 0197 if (!ok) { 0198 // maybe it's a textual date 0199 unsigned i = 1; 0200 while (i <= 12) { 0201 if (KGlobal::locale()->calendar()->monthName(i, 2000).toLower() == *it_scanned 0202 || KGlobal::locale()->calendar()->monthName(i, 2000, 0203 KCalendarSystem::ShortName). 0204 toLower() == *it_scanned) { 0205 month = i; 0206 } 0207 ++i; 0208 } 0209 } 0210 0211 if (month < 1 || month > 12) { 0212 return setError(AlkDateFormat::InvalidMonth, *it_scanned); 0213 } 0214 0215 break; 0216 case 'y': 0217 if (_strict && (*it_scanned).length() != (*it_format).length()) { 0218 return setError(AlkDateFormat::InvalidYearLength, *it_scanned, *it_format); 0219 } 0220 0221 year = (*it_scanned).toUInt(&ok); 0222 0223 if (!ok) { 0224 return setError(AlkDateFormat::InvalidYear, *it_scanned); 0225 } 0226 0227 // 0228 // 2-digit year case 0229 // 0230 // this algorithm will pick a year within +/- 50 years of the 0231 // centurymidpoint parameter. i.e. if the midpoint is 2000, 0232 // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999 0233 if (year < 100) { 0234 unsigned centuryend = _centurymidpoint + 50; 0235 unsigned centurybegin = _centurymidpoint - 50; 0236 0237 if (year < centuryend % 100) { 0238 year += 100; 0239 } 0240 year += centurybegin - centurybegin % 100; 0241 } 0242 0243 if (year < 1900) { 0244 return setError(AlkDateFormat::InvalidYear, QString::number(year)); 0245 } 0246 0247 break; 0248 default: 0249 return setError(AlkDateFormat::InvalidFormatCharacter, QString((*it_format).at(0).cell())); 0250 } 0251 0252 ++it_scanned; 0253 ++it_format; 0254 } 0255 QDate result(year, month, day); 0256 if (!result.isValid()) { 0257 return setError(AlkDateFormat::InvalidDate, QString("yr:%1 mo:%2 dy:%3)").arg(year).arg(month).arg(day)); 0258 } 0259 0260 return result; 0261 } 0262 0263 #else // Qt5 0264 0265 QDate convertStringKMyMoney(const QString& _in, bool _strict, unsigned _centurymidpoint) 0266 { 0267 // 0268 // Break date format string into component parts 0269 // 0270 0271 QRegularExpression formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", QRegularExpression::CaseInsensitiveOption); 0272 QRegularExpressionMatch match = formatrex.match(m_format); 0273 if (!match.hasMatch()) { 0274 return setError(AlkDateFormat::InvalidFormatString, m_format); 0275 } 0276 0277 QStringList formatParts; 0278 formatParts += match.captured(1); 0279 formatParts += match.captured(3); 0280 formatParts += match.captured(5); 0281 0282 QStringList formatDelimiters; 0283 formatDelimiters += match.captured(2); 0284 formatDelimiters += match.captured(4); 0285 0286 // make sure to escape delimiters that are special chars in regex 0287 QStringList::iterator it; 0288 QRegularExpression specialChars("^[\\.\\\\\\?]$"); 0289 for(it = formatDelimiters.begin(); it != formatDelimiters.end(); ++it) { 0290 QRegularExpressionMatch special = specialChars.match(*it); 0291 if (special.hasMatch()) { 0292 (*it).prepend("\\"); 0293 } 0294 } 0295 0296 // 0297 // Break input string up into component parts, 0298 // using the delimiters found in the format string 0299 // 0300 QRegularExpression inputrex; 0301 inputrex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); 0302 0303 // strict mode means we must enforce the delimiters as specified in the 0304 // format. non-strict allows any delimiters 0305 if (_strict) 0306 inputrex.setPattern(QString("(\\w+)\\.?%1(\\w+)\\.?%2(\\w+)\\.?").arg(formatDelimiters[0], formatDelimiters[1])); 0307 else 0308 inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)"); 0309 0310 match = inputrex.match(_in); 0311 if (!match.hasMatch()) { 0312 return setError(AlkDateFormat::InvalidDate, _in); 0313 } 0314 0315 QStringList scannedParts; 0316 scannedParts += match.captured(1).toLower(); 0317 scannedParts += match.captured(2).toLower(); 0318 scannedParts += match.captured(3).toLower(); 0319 0320 // 0321 // Convert the scanned parts into actual date components 0322 // 0323 unsigned day = 0, month = 0, year = 0; 0324 bool ok; 0325 QRegularExpression digitrex("(\\d+)"); 0326 QStringList::const_iterator it_scanned = scannedParts.constBegin(); 0327 QStringList::const_iterator it_format = formatParts.constBegin(); 0328 while (it_scanned != scannedParts.constEnd()) { 0329 // decide upon the first character of the part 0330 switch ((*it_format).at(0).cell()) { 0331 case 'd': 0332 // remove any extraneous non-digits (e.g. read "3rd" as 3) 0333 ok = false; 0334 match = digitrex.match(*it_scanned); 0335 if (match.hasMatch()) 0336 day = match.captured(1).toUInt(&ok); 0337 if (!ok || day > 31) 0338 return setError(AlkDateFormat::InvalidDay, *it_scanned); 0339 break; 0340 case 'm': 0341 month = (*it_scanned).toUInt(&ok); 0342 if (!ok) { 0343 month = 0; 0344 // maybe it's a textual date 0345 unsigned i = 1; 0346 // search the name in the current selected locale 0347 QLocale locale; 0348 while (i <= 12) { 0349 if (locale.standaloneMonthName(i).toLower() == *it_scanned 0350 || locale.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) { 0351 month = i; 0352 break; 0353 } 0354 ++i; 0355 } 0356 // in case we did not find the month in the current locale, 0357 // we look for it in the C locale 0358 if(month == 0) { 0359 QLocale localeC(QLocale::C); 0360 if( !(locale == localeC)) { 0361 i = 1; 0362 while (i <= 12) { 0363 if (localeC.standaloneMonthName(i).toLower() == *it_scanned 0364 || localeC.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) { 0365 month = i; 0366 break; 0367 } 0368 ++i; 0369 } 0370 } 0371 } 0372 } 0373 0374 if (month < 1 || month > 12) 0375 return setError(AlkDateFormat::InvalidMonth, *it_scanned); 0376 0377 break; 0378 case 'y': 0379 if (_strict && (*it_scanned).length() != (*it_format).length()) 0380 return setError(AlkDateFormat::InvalidYearLength, *it_scanned, *it_format); 0381 0382 year = (*it_scanned).toUInt(&ok); 0383 0384 if (!ok) 0385 return setError(AlkDateFormat::InvalidYear, *it_scanned); 0386 0387 // 0388 // 2-digit year case 0389 // 0390 // this algorithm will pick a year within +/- 50 years of the 0391 // centurymidpoint parameter. i.e. if the midpoint is 2000, 0392 // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999 0393 if (year < 100) { 0394 unsigned centuryend = _centurymidpoint + 50; 0395 unsigned centurybegin = _centurymidpoint - 50; 0396 0397 if (year < centuryend % 100) 0398 year += 100; 0399 year += centurybegin - centurybegin % 100; 0400 } 0401 0402 if (year < 1900) 0403 return setError(AlkDateFormat::InvalidYear, QString::number(year)); 0404 0405 break; 0406 default: 0407 return setError(AlkDateFormat::InvalidFormatCharacter, QString(QChar((*it_format).at(0).cell()))); 0408 } 0409 0410 ++it_scanned; 0411 ++it_format; 0412 } 0413 QDate result(year, month, day); 0414 if (! result.isValid()) 0415 return setError(AlkDateFormat::InvalidDate, QString("yr:%1 mo:%2 dy:%3)").arg(year).arg(month).arg(day)); 0416 0417 return result; 0418 } 0419 #endif 0420 0421 }; 0422 0423 AlkDateFormat::AlkDateFormat(const QString &format) 0424 : d(new Private) 0425 { 0426 d->m_format = format; 0427 d->m_errorCode = NoError; 0428 } 0429 0430 AlkDateFormat::AlkDateFormat(const AlkDateFormat& right) 0431 : d(new Private) 0432 { 0433 d->m_format = right.d->m_format; 0434 d->m_errorCode = NoError; 0435 } 0436 0437 AlkDateFormat::~AlkDateFormat() 0438 { 0439 delete d; 0440 } 0441 0442 AlkDateFormat& AlkDateFormat::operator=(const AlkDateFormat& right) 0443 { 0444 d->m_format = right.d->m_format; 0445 0446 return *this; 0447 } 0448 0449 const QString & AlkDateFormat::format() const 0450 { 0451 return d->m_format; 0452 } 0453 0454 AlkDateFormat::ErrorCode AlkDateFormat::lastError() const 0455 { 0456 return d->m_errorCode; 0457 } 0458 0459 QString AlkDateFormat::lastErrorMessage() const 0460 { 0461 return d->m_errorMessage; 0462 } 0463 0464 0465 QDate AlkDateFormat::convertString(const QString& date, bool strict, unsigned int centuryMidPoint) 0466 { 0467 // reset any pending errors from previous runs 0468 d->m_errorCode = NoError; 0469 d->m_errorMessage.clear(); 0470 0471 if (d->m_format.contains("%")) 0472 return d->convertStringKMyMoney(date, strict, centuryMidPoint); 0473 else 0474 return d->convertStringSkrooge(date); 0475 } 0476 0477 QString AlkDateFormat::convertDate(const QDate& date) 0478 { 0479 Q_UNUSED(date); 0480 0481 // reset any pending errors from previous runs 0482 d->m_errorCode = NoError; 0483 d->m_errorMessage.clear(); 0484 0485 return QString(); 0486 }