File indexing completed on 2024-04-28 05:02:30

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 }