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 }