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