File indexing completed on 2024-05-19 05:07:21

0001 /*
0002     SPDX-FileCopyrightText: 2002 Thomas Baumgart <thb@net-bembel.de>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "mymoneyqifprofile.h"
0007 
0008 // ----------------------------------------------------------------------------
0009 // QT Includes
0010 
0011 #include <QList>
0012 #include <QLocale>
0013 #include <QRegularExpression>
0014 #include <QVector>
0015 
0016 // ----------------------------------------------------------------------------
0017 // KDE Includes
0018 
0019 #include <KConfigGroup>
0020 #include <KSharedConfig>
0021 
0022 // ----------------------------------------------------------------------------
0023 // Project Includes
0024 
0025 #include "mymoneyenums.h"
0026 #include "mymoneyexception.h"
0027 #include "mymoneymoney.h"
0028 
0029 /*
0030  * CENTURY_BREAK is used to identify the century for a two digit year
0031  *
0032  * if yr is < CENTURY_BREAK it is in 2000
0033  * if yr is >= CENTURY_BREAK it is in 1900
0034  *
0035  * so with CENTURY_BREAK being 70 the following will happen:
0036  *
0037  *  00..69  ->  2000..2069
0038  *  70..99  ->  1970..1999
0039  */
0040 const int CENTURY_BREAK = 70;
0041 
0042 class MyMoneyQifProfile::Private
0043 {
0044 public:
0045     Private()
0046         : m_changeCount(3, 0)
0047         , m_lastValue(3, 0)
0048         , m_largestValue(3, 0)
0049     {
0050     }
0051 
0052     void getThirdPosition();
0053     void dissectDate(QVector<QString>& parts, const QString& txt) const;
0054 
0055     QVector<int> m_changeCount;
0056     QVector<int> m_lastValue;
0057     QVector<int> m_largestValue;
0058     QMap<QChar, int> m_partPos;
0059 };
0060 
0061 void MyMoneyQifProfile::Private::dissectDate(QVector<QString>& parts, const QString& txt) const
0062 {
0063     static const QRegularExpression nonDelimChars(QLatin1String("[ 0-9a-zA-Z]"));
0064     int part = 0; // the current part we scan
0065     int pos; // the current scan position
0066     int maxPartSize = txt.length() > 6 ? 4 : 2;
0067     // the maximum size of a part
0068     // some fu... up MS-Money versions write two delimiter in a row
0069     // so we need to keep track of them. Example: D14/12/'08
0070     bool lastWasDelim = false;
0071 
0072     // separate the parts of the date and keep the locations of the delimiters
0073     for (pos = 0; pos < txt.length() && part < 3; ++pos) {
0074         const auto nonDelimMatch(nonDelimChars.match(txt[pos]));
0075         if (!nonDelimMatch.hasMatch()) {
0076             if (!lastWasDelim) {
0077                 ++part;
0078                 maxPartSize = 0; // make sure to pick the right one depending if next char is numeric or not
0079                 lastWasDelim = true;
0080             }
0081         } else {
0082             lastWasDelim = false;
0083             // check if the part is over and we did not see a delimiter
0084             if ((maxPartSize != 0) && (parts[part].length() == maxPartSize)) {
0085                 ++part;
0086                 maxPartSize = 0;
0087             }
0088             if (maxPartSize == 0) {
0089                 maxPartSize = txt[pos].isDigit() ? 2 : 3;
0090                 if (part == 2)
0091                     maxPartSize = 4;
0092             }
0093             if (part < 3)
0094                 parts[part] += txt[pos];
0095         }
0096     }
0097 
0098     if (part == 3) { // invalid date
0099         for (int i = 0; i < 3; ++i) {
0100             parts[i] = '0';
0101         }
0102     }
0103 }
0104 
0105 void MyMoneyQifProfile::Private::getThirdPosition()
0106 {
0107     // if we have detected two parts we can calculate the third and its position
0108     if (m_partPos.count() == 2) {
0109         QList<QChar> partsPresent = m_partPos.keys();
0110         QStringList partsAvail = QString("d,m,y").split(',');
0111         int missingIndex = -1;
0112         int value = 0;
0113         for (int i = 0; i < 3; ++i) {
0114             if (!partsPresent.contains(partsAvail[i][0])) {
0115                 missingIndex = i;
0116             } else {
0117                 value += m_partPos[partsAvail[i][0]];
0118             }
0119         }
0120         m_partPos[partsAvail[missingIndex][0]] = 3 - value;
0121     }
0122 }
0123 
0124 MyMoneyQifProfile::MyMoneyQifProfile()
0125     : d(new Private)
0126     , m_isDirty(false)
0127 {
0128     clear();
0129 }
0130 
0131 MyMoneyQifProfile::MyMoneyQifProfile(const QString& name)
0132     : d(new Private)
0133     , m_isDirty(false)
0134 {
0135     loadProfile(name);
0136 }
0137 
0138 MyMoneyQifProfile::~MyMoneyQifProfile()
0139 {
0140     delete d;
0141 }
0142 
0143 void MyMoneyQifProfile::clear()
0144 {
0145     m_dateFormat = QLatin1String("%d.%m.%yyyy");
0146     m_apostropheFormat = QLatin1String("2000-2099");
0147     m_valueMode.clear();
0148     m_filterScriptImport.clear();
0149     m_filterScriptExport.clear();
0150     m_filterFileType = QLatin1String("*.qif *.QIF");
0151 
0152     m_decimal.clear();
0153 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0154     m_decimal['$'] = m_decimal['Q'] = m_decimal['T'] = m_decimal['O'] = m_decimal['I'] = QLocale().decimalPoint();
0155 #else
0156     m_decimal['$'] = m_decimal['Q'] = m_decimal['T'] = m_decimal['O'] = m_decimal['I'] = QLocale().decimalPoint().at(0);
0157 #endif
0158 
0159     m_thousands.clear();
0160 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0161     m_thousands['$'] = m_thousands['Q'] = m_thousands['T'] = m_thousands['O'] = m_thousands['I'] = QLocale().groupSeparator();
0162 #else
0163     m_thousands['$'] = m_thousands['Q'] = m_thousands['T'] = m_thousands['O'] = m_thousands['I'] = QLocale().groupSeparator().at(0);
0164 #endif
0165 
0166     m_openingBalanceText = QLatin1String("Opening Balance");
0167     m_voidMark = QLatin1String("VOID ");
0168     m_accountDelimiter = '[';
0169 
0170     m_profileName.clear();
0171     m_profileDescription.clear();
0172     m_profileType = QLatin1String("Bank");
0173 
0174     m_attemptMatchDuplicates = true;
0175 }
0176 
0177 void MyMoneyQifProfile::loadProfile(const QString& name)
0178 {
0179     clear();
0180 
0181     setProfileName(name);
0182 
0183     KSharedConfigPtr config = KSharedConfig::openConfig();
0184     KConfigGroup grp = config->group(m_profileName);
0185 
0186     m_profileDescription = grp.readEntry("Description", m_profileDescription);
0187     m_profileType = grp.readEntry("Type", m_profileType);
0188     m_dateFormat = grp.readEntry("DateFormat", m_dateFormat);
0189     m_apostropheFormat = grp.readEntry("ApostropheFormat", m_apostropheFormat);
0190     m_accountDelimiter = grp.readEntry("AccountDelimiter", m_accountDelimiter);
0191     m_openingBalanceText = grp.readEntry("OpeningBalance", m_openingBalanceText);
0192     m_voidMark = grp.readEntry("VoidMark", m_voidMark);
0193     m_filterScriptImport = grp.readEntry("FilterScriptImport", m_filterScriptImport);
0194     m_filterScriptExport = grp.readEntry("FilterScriptExport", m_filterScriptExport);
0195     m_filterFileType = grp.readEntry("FilterFileType", m_filterFileType);
0196 
0197     m_attemptMatchDuplicates = grp.readEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates);
0198 
0199     // make sure, we remove any old stuff for now
0200     grp.deleteEntry("FilterScript");
0201 
0202     QString tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] + m_decimal['$'] + m_decimal['O'];
0203     tmp = grp.readEntry("Decimal", tmp);
0204     m_decimal['Q'] = tmp[0];
0205     m_decimal['T'] = tmp[1];
0206     m_decimal['I'] = tmp[2];
0207     m_decimal['$'] = tmp[3];
0208     m_decimal['O'] = tmp[4];
0209 
0210     tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] + m_thousands['$'] + m_thousands['O'];
0211     tmp = grp.readEntry("Thousand", tmp);
0212     m_thousands['Q'] = tmp[0];
0213     m_thousands['T'] = tmp[1];
0214     m_thousands['I'] = tmp[2];
0215     m_thousands['$'] = tmp[3];
0216     m_thousands['O'] = tmp[4];
0217 
0218     m_isDirty = false;
0219 }
0220 
0221 void MyMoneyQifProfile::saveProfile()
0222 {
0223     if (m_isDirty == true) {
0224         KSharedConfigPtr config = KSharedConfig::openConfig();
0225         KConfigGroup grp = config->group(m_profileName);
0226 
0227         grp.writeEntry("Description", m_profileDescription);
0228         grp.writeEntry("Type", m_profileType);
0229         grp.writeEntry("DateFormat", m_dateFormat);
0230         grp.writeEntry("ApostropheFormat", m_apostropheFormat);
0231         grp.writeEntry("AccountDelimiter", m_accountDelimiter);
0232         grp.writeEntry("OpeningBalance", m_openingBalanceText);
0233         grp.writeEntry("VoidMark", m_voidMark);
0234         grp.writeEntry("FilterScriptImport", m_filterScriptImport);
0235         grp.writeEntry("FilterScriptExport", m_filterScriptExport);
0236         grp.writeEntry("FilterFileType", m_filterFileType);
0237         grp.writeEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates);
0238 
0239         QString tmp;
0240 
0241         tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] + m_decimal['$'] + m_decimal['O'];
0242         grp.writeEntry("Decimal", tmp);
0243         tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] + m_thousands['$'] + m_thousands['O'];
0244         grp.writeEntry("Thousand", tmp);
0245     }
0246     m_isDirty = false;
0247 }
0248 
0249 void MyMoneyQifProfile::setProfileName(const QString& name)
0250 {
0251     const auto internalName = QStringLiteral("Profile-%1").arg(name);
0252 
0253     if (m_profileName != internalName)
0254         m_isDirty = true;
0255 
0256     m_profileName = internalName;
0257 }
0258 
0259 void MyMoneyQifProfile::setProfileDescription(const QString& desc)
0260 {
0261     if (m_profileDescription != desc)
0262         m_isDirty = true;
0263 
0264     m_profileDescription = desc;
0265 }
0266 
0267 void MyMoneyQifProfile::setProfileType(const QString& type)
0268 {
0269     if (m_profileType != type)
0270         m_isDirty = true;
0271     m_profileType = type;
0272 }
0273 
0274 void MyMoneyQifProfile::setOutputDateFormat(const QString& dateFormat)
0275 {
0276     if (m_dateFormat != dateFormat)
0277         m_isDirty = true;
0278 
0279     m_dateFormat = dateFormat;
0280 }
0281 
0282 void MyMoneyQifProfile::setInputDateFormat(const QString& dateFormat)
0283 {
0284     int j = -1;
0285     if (dateFormat.length() > 0) {
0286         for (int i = 0; i < dateFormat.length() - 1; ++i) {
0287             if (dateFormat[i] == '%') {
0288                 d->m_partPos[dateFormat[++i]] = ++j;
0289             }
0290         }
0291     }
0292 }
0293 
0294 void MyMoneyQifProfile::setApostropheFormat(const QString& apostropheFormat)
0295 {
0296     if (m_apostropheFormat != apostropheFormat)
0297         m_isDirty = true;
0298 
0299     m_apostropheFormat = apostropheFormat;
0300 }
0301 
0302 void MyMoneyQifProfile::setAmountDecimal(const QChar& def, const QChar& chr)
0303 {
0304     QChar ch(chr);
0305     if (ch == QChar())
0306         ch = ' ';
0307 
0308     if (m_decimal[def] != ch)
0309         m_isDirty = true;
0310 
0311     m_decimal[def] = ch;
0312 }
0313 
0314 void MyMoneyQifProfile::setAmountThousands(const QChar& def, const QChar& chr)
0315 {
0316     QChar ch(chr);
0317     if (ch == QChar())
0318         ch = ' ';
0319 
0320     if (m_thousands[def] != ch)
0321         m_isDirty = true;
0322 
0323     m_thousands[def] = ch;
0324 }
0325 
0326 QChar MyMoneyQifProfile::amountDecimal(const QChar& def) const
0327 {
0328     QChar chr = m_decimal[def];
0329     return chr;
0330 }
0331 
0332 QChar MyMoneyQifProfile::amountThousands(const QChar& def) const
0333 {
0334     QChar chr = m_thousands[def];
0335     return chr;
0336 }
0337 
0338 void MyMoneyQifProfile::setAccountDelimiter(const QString& delim)
0339 {
0340     QString txt(delim);
0341 
0342     if (txt.isEmpty())
0343         txt = ' ';
0344     else if (txt[0] != '[')
0345         txt = '[';
0346 
0347     if (m_accountDelimiter[0] != txt[0])
0348         m_isDirty = true;
0349     m_accountDelimiter = txt[0];
0350 }
0351 
0352 void MyMoneyQifProfile::setOpeningBalanceText(const QString& txt)
0353 {
0354     if (m_openingBalanceText != txt)
0355         m_isDirty = true;
0356     m_openingBalanceText = txt;
0357 }
0358 
0359 void MyMoneyQifProfile::setVoidMark(const QString& txt)
0360 {
0361     if (m_voidMark != txt)
0362         m_isDirty = true;
0363     m_voidMark = txt;
0364 }
0365 
0366 QString MyMoneyQifProfile::accountDelimiter() const
0367 {
0368     QString rc;
0369     if (m_accountDelimiter[0] == ' ')
0370         rc = ' ';
0371     else
0372         rc = "[]";
0373     return rc;
0374 }
0375 
0376 QString MyMoneyQifProfile::date(const QDate& datein) const
0377 {
0378     QString::const_iterator format = m_dateFormat.begin();
0379     QString buffer;
0380     QChar delim;
0381     int maskLen;
0382     QChar maskChar;
0383 
0384     while (format != m_dateFormat.end()) {
0385         if (*format == '%') {
0386             maskLen = 0;
0387             maskChar = *(++format);
0388             while ((format != m_dateFormat.end()) && (*format == maskChar)) {
0389                 ++maskLen;
0390                 ++format;
0391             }
0392 
0393             if (maskChar == 'd') {
0394                 if (!delim.isNull())
0395                     buffer += delim;
0396                 buffer += QString::number(datein.day()).rightJustified(2, '0');
0397 
0398             } else if (maskChar == 'm') {
0399                 if (!delim.isNull())
0400                     buffer += delim;
0401                 if (maskLen == 3)
0402                     buffer += QLocale().monthName(datein.month(), QLocale::ShortFormat);
0403                 else
0404                     buffer += QString::number(datein.month()).rightJustified(2, '0');
0405             } else if (maskChar == 'y') {
0406                 if (maskLen == 2) {
0407                     buffer += twoDigitYear(delim, datein.year());
0408                 } else {
0409                     if (!delim.isNull())
0410                         buffer += delim;
0411                     buffer += QString::number(datein.year());
0412                 }
0413             } else {
0414                 throw MYMONEYEXCEPTION_CSTRING("Invalid char in QifProfile date field");
0415             }
0416             delim = QLatin1Char(0);
0417         } else {
0418             if (!delim.isNull())
0419                 buffer += delim;
0420             delim = *format++;
0421         }
0422     }
0423     return buffer;
0424 }
0425 
0426 QDate MyMoneyQifProfile::date(const QString& datein) const
0427 {
0428     // in case we don't know the format, we return an invalid date
0429     if (d->m_partPos.count() != 3)
0430         return QDate();
0431 
0432     QVector<QString> scannedParts(3);
0433     d->dissectDate(scannedParts, datein);
0434 
0435     int yr, mon, day;
0436     bool ok;
0437     yr = scannedParts[d->m_partPos['y']].toInt();
0438     mon = scannedParts[d->m_partPos['m']].toInt(&ok);
0439     if (!ok) {
0440         QStringList monthNames = QString("jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec").split(',');
0441         int j;
0442         for (j = 1; j <= 12; ++j) {
0443             if ((QLocale().monthName(j, QLocale::ShortFormat).toLower() == scannedParts[d->m_partPos['m']].toLower())
0444                 || (monthNames[j - 1] == scannedParts[d->m_partPos['m']].toLower())) {
0445                 mon = j;
0446                 break;
0447             }
0448         }
0449         if (j == 13) {
0450             qWarning("Unknown month '%s'", qPrintable(scannedParts[d->m_partPos['m']]));
0451             return QDate();
0452         }
0453     }
0454 
0455     day = scannedParts[d->m_partPos['d']].toInt();
0456     if (yr < 100) { // two digit year information?
0457         if (yr < CENTURY_BREAK) // less than the CENTURY_BREAK we assume this century
0458             yr += 2000;
0459         else
0460             yr += 1900;
0461     }
0462     return QDate(yr, mon, day);
0463 
0464 #if 0
0465     QString scannedDelim[2];
0466     QString formatParts[3];
0467     QString formatDelim[2];
0468     int part;
0469     int delim;
0470     unsigned int i, j;
0471 
0472     part = -1;
0473     delim = 0;
0474     for (i = 0; i < m_dateFormat.length(); ++i) {
0475         if (m_dateFormat[i] == '%') {
0476             ++part;
0477             if (part == 3) {
0478                 qWarning("MyMoneyQifProfile::date(const QString& datein) Too many parts in date format");
0479                 return QDate();
0480             }
0481             ++i;
0482         }
0483         switch (m_dateFormat[i].toLatin1()) {
0484         case 'm':
0485         case 'd':
0486         case 'y':
0487             formatParts[part] += m_dateFormat[i];
0488             break;
0489         case '/':
0490         case '-':
0491         case '.':
0492         case '\'':
0493             if (delim == 2) {
0494                 qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date format");
0495                 return QDate();
0496             }
0497             formatDelim[delim] = m_dateFormat[i];
0498             ++delim;
0499             break;
0500         default:
0501             qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid char in date format");
0502             return QDate();
0503         }
0504     }
0505 
0506 
0507     part = 0;
0508     delim = 0;
0509     bool prevWasChar = false;
0510     for (i = 0; i < datein.length(); ++i) {
0511         switch (datein[i].toLatin1()) {
0512         case '/':
0513         case '.':
0514         case '-':
0515         case '\'':
0516             if (delim == 2) {
0517                 qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date field");
0518                 return QDate();
0519             }
0520             scannedDelim[delim] = datein[i];
0521             ++delim;
0522             ++part;
0523             prevWasChar = false;
0524             break;
0525 
0526         default:
0527             if (prevWasChar && datein[i].isDigit()) {
0528                 ++part;
0529                 prevWasChar = false;
0530             }
0531             if (datein[i].isLetter())
0532                 prevWasChar = true;
0533             // replace blank with 0
0534             scannedParts[part] += (datein[i] == ' ') ? QChar('0') : datein[i];
0535             break;
0536         }
0537     }
0538 
0539     int day = 1,
0540         mon = 1,
0541         yr = 1900;
0542     bool ok = false;
0543     for (i = 0; i < 2; ++i) {
0544         if (scannedDelim[i] != formatDelim[i]
0545                 && scannedDelim[i] != QChar('\'')) {
0546             qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid delimiter '%s' when '%s' was expected",
0547                      scannedDelim[i].toLatin1(), formatDelim[i].toLatin1());
0548             return QDate();
0549         }
0550     }
0551 
0552     QString msg;
0553     for (i = 0; i < 3; ++i) {
0554         switch (formatParts[i][0].toLatin1()) {
0555         case 'd':
0556             day = scannedParts[i].toUInt(&ok);
0557             if (!ok)
0558                 msg = "Invalid numeric character in day string";
0559             break;
0560         case 'm':
0561             if (formatParts[i].length() != 3) {
0562                 mon = scannedParts[i].toUInt(&ok);
0563                 if (!ok)
0564                     msg = "Invalid numeric character in month string";
0565             } else {
0566                 for (j = 1; j <= 12; ++j) {
0567                     if (QLocale().monthName(j, 2000, true).toLower() == formatParts[i].toLower()) {
0568                         mon = j;
0569                         ok = true;
0570                         break;
0571                     }
0572                 }
0573                 if (j == 13) {
0574                     msg = "Unknown month '" + scannedParts[i] + "'";
0575                 }
0576             }
0577             break;
0578         case 'y':
0579             ok = false;
0580             if (scannedParts[i].length() == formatParts[i].length()) {
0581                 yr = scannedParts[i].toUInt(&ok);
0582                 if (!ok)
0583                     msg = "Invalid numeric character in month string";
0584                 if (yr < 100) {     // two digit year info
0585                     if (i > 1) {
0586                         ok = true;
0587                         if (scannedDelim[i-1] == QChar('\'')) {
0588                             if (m_apostropheFormat == "1900-1949") {
0589                                 if (yr < 50)
0590                                     yr += 1900;
0591                                 else
0592                                     yr += 2000;
0593                             } else if (m_apostropheFormat == "1900-1999") {
0594                                 yr += 1900;
0595                             } else if (m_apostropheFormat == "2000-2099") {
0596                                 yr += 2000;
0597                             } else {
0598                                 msg = "Unsupported apostropheFormat!";
0599                                 ok = false;
0600                             }
0601                         } else {
0602                             if (m_apostropheFormat == "1900-1949") {
0603                                 if (yr < 50)
0604                                     yr += 2000;
0605                                 else
0606                                     yr += 1900;
0607                             } else if (m_apostropheFormat == "1900-1999") {
0608                                 yr += 2000;
0609                             } else if (m_apostropheFormat == "2000-2099") {
0610                                 yr += 1900;
0611                             } else {
0612                                 msg = "Unsupported apostropheFormat!";
0613                                 ok = false;
0614                             }
0615                         }
0616                     } else {
0617                         msg = "Year as first parameter is not supported!";
0618                     }
0619                 } else if (yr < 1900) {
0620                     msg = "Year not in range < 100 or >= 1900!";
0621                 } else {
0622                     ok = true;
0623                 }
0624             } else {
0625                 msg = QString("Length of year (%1) does not match expected length (%2).")
0626                       .arg(scannedParts[i].length()).arg(formatParts[i].length());
0627             }
0628             break;
0629         }
0630         if (!msg.isEmpty()) {
0631             qWarning("MyMoneyQifProfile::date(const QString& datein) %s", msg.toLatin1());
0632             return QDate();
0633         }
0634     }
0635     return QDate(yr, mon, day);
0636 #endif
0637 }
0638 
0639 const QString MyMoneyQifProfile::twoDigitYear(const QChar& delim, int yr) const
0640 {
0641     QChar realDelim = delim;
0642     QString buffer;
0643     if (!delim.isNull()) {
0644         if ((m_apostropheFormat == "1900-1949" && yr <= 1949) || (m_apostropheFormat == "1900-1999" && yr <= 1999)
0645             || (m_apostropheFormat == "2000-2099" && yr >= 2000))
0646             realDelim = '\'';
0647         buffer += realDelim;
0648     }
0649     yr -= 1900;
0650     if (yr > 100)
0651         yr -= 100;
0652 
0653     if (yr < 10)
0654         buffer += '0';
0655 
0656     buffer += QString::number(yr);
0657     return buffer;
0658 }
0659 
0660 QString MyMoneyQifProfile::value(const QChar& def, const MyMoneyMoney& valuein) const
0661 {
0662     QChar _decimalSeparator;
0663     QChar _thousandsSeparator;
0664     QString res;
0665 
0666     _decimalSeparator = MyMoneyMoney::decimalSeparator();
0667     _thousandsSeparator = MyMoneyMoney::thousandSeparator();
0668     eMyMoney::Money::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition();
0669 
0670     MyMoneyMoney::setDecimalSeparator(amountDecimal(def).toLatin1());
0671     MyMoneyMoney::setThousandSeparator(amountThousands(def).toLatin1());
0672     MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::PreceedQuantityAndSymbol);
0673     res = valuein.formatMoney("", 2);
0674 
0675     MyMoneyMoney::setDecimalSeparator(_decimalSeparator);
0676     MyMoneyMoney::setThousandSeparator(_thousandsSeparator);
0677     MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition);
0678 
0679     return res;
0680 }
0681 
0682 MyMoneyMoney MyMoneyQifProfile::value(const QChar& def, const QString& valuein) const
0683 {
0684     QChar _decimalSeparator;
0685     QChar _thousandsSeparator;
0686     MyMoneyMoney res;
0687 
0688     _decimalSeparator = MyMoneyMoney::decimalSeparator();
0689     _thousandsSeparator = MyMoneyMoney::thousandSeparator();
0690     eMyMoney::Money::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition();
0691 
0692     MyMoneyMoney::setDecimalSeparator(amountDecimal(def).toLatin1());
0693     MyMoneyMoney::setThousandSeparator(amountThousands(def).toLatin1());
0694     MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::PreceedQuantityAndSymbol);
0695 
0696     res = MyMoneyMoney(valuein);
0697 
0698     MyMoneyMoney::setDecimalSeparator(_decimalSeparator);
0699     MyMoneyMoney::setThousandSeparator(_thousandsSeparator);
0700     MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition);
0701 
0702     return res;
0703 }
0704 
0705 void MyMoneyQifProfile::setFilterScriptImport(const QString& script)
0706 {
0707     if (m_filterScriptImport != script)
0708         m_isDirty = true;
0709 
0710     m_filterScriptImport = script;
0711 }
0712 
0713 void MyMoneyQifProfile::setFilterScriptExport(const QString& script)
0714 {
0715     if (m_filterScriptExport != script)
0716         m_isDirty = true;
0717 
0718     m_filterScriptExport = script;
0719 }
0720 
0721 void MyMoneyQifProfile::setFilterFileType(const QString& txt)
0722 {
0723     if (m_filterFileType != txt)
0724         m_isDirty = true;
0725 
0726     m_filterFileType = txt;
0727 }
0728 
0729 void MyMoneyQifProfile::setAttemptMatchDuplicates(bool f)
0730 {
0731     if (m_attemptMatchDuplicates != f)
0732         m_isDirty = true;
0733 
0734     m_attemptMatchDuplicates = f;
0735 }
0736 
0737 QString MyMoneyQifProfile::inputDateFormat() const
0738 {
0739     QStringList list;
0740     possibleDateFormats(list);
0741     if (list.count() == 1)
0742         return list.first();
0743     return QString();
0744 }
0745 
0746 void MyMoneyQifProfile::possibleDateFormats(QStringList& list) const
0747 {
0748     QStringList defaultList = QString("y,m,d:y,d,m:m,d,y:m,y,d:d,m,y:d,y,m").split(':');
0749     list.clear();
0750     QStringList::const_iterator it_d;
0751     for (it_d = defaultList.constBegin(); it_d != defaultList.constEnd(); ++it_d) {
0752         const QStringList parts = (*it_d).split(',', Qt::SkipEmptyParts);
0753         int i;
0754         for (i = 0; i < 3; ++i) {
0755             if (d->m_partPos.contains(parts[i][0])) {
0756                 if (d->m_partPos[parts[i][0]] != i)
0757                     break;
0758             }
0759             // months can't be larger than 12
0760             if (parts[i] == "m" && d->m_largestValue[i] > 12)
0761                 break;
0762             // days can't be larger than 31
0763             if (parts[i] == "d" && d->m_largestValue[i] > 31)
0764                 break;
0765         }
0766         // matches all tests
0767         if (i == 3) {
0768             QString format = *it_d;
0769             format.replace('y', "%y");
0770             format.replace('m', "%m");
0771             format.replace('d', "%d");
0772             format.replace(',', " ");
0773             list << format;
0774         }
0775     }
0776     // if we haven't found any, then there's something wrong.
0777     // in this case, we present the full list and let the user decide
0778     if (list.count() == 0) {
0779         for (it_d = defaultList.constBegin(); it_d != defaultList.constEnd(); ++it_d) {
0780             QString format = *it_d;
0781             format.replace('y', "%y");
0782             format.replace('m', "%m");
0783             format.replace('d', "%d");
0784             format.replace(',', " ");
0785             list << format;
0786         }
0787     }
0788 }
0789 
0790 void MyMoneyQifProfile::autoDetect(const QStringList& lines)
0791 {
0792     m_dateFormat.clear();
0793     m_decimal.clear();
0794     m_thousands.clear();
0795 
0796     QString numericRecords = "BT$OIQ";
0797     QStringList::const_iterator it;
0798     int datesScanned = 0;
0799     // section: used to switch between different QIF sections,
0800     // because the Record identifiers are ambiguous between sections
0801     // eg. in transaction records, T identifies a total amount, in
0802     // account sections it's the type.
0803     //
0804     // 0 - unknown
0805     // 1 - account
0806     // 2 - transactions
0807     // 3 - prices
0808     int section = 0;
0809     static const QRegularExpression priceExp(QLatin1String("\"(.*)\",(.*),\"(.*)\""));
0810     QRegularExpressionMatch priceMatch;
0811     for (it = lines.begin(); it != lines.end(); ++it) {
0812         QChar c((*it)[0]);
0813         if (c == '!') {
0814             QString sname = (*it).toLower();
0815             if (!sname.startsWith(QLatin1String("!option:"))) {
0816                 section = 0;
0817                 if (sname.startsWith(QLatin1String("!account")))
0818                     section = 1;
0819                 else if (sname.startsWith(QLatin1String("!type"))) {
0820                     if (sname.startsWith(QLatin1String("!type:cat")) || sname.startsWith(QLatin1String("!type:payee"))
0821                         || sname.startsWith(QLatin1String("!type:security")) || sname.startsWith(QLatin1String("!type:class"))) {
0822                         section = 0;
0823                     } else if (sname.startsWith(QLatin1String("!type:price"))) {
0824                         section = 3;
0825                     } else
0826                         section = 2;
0827                 }
0828             }
0829         }
0830 
0831         switch (section) {
0832         case 1:
0833             if (c == 'B') {
0834                 scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]);
0835             }
0836             break;
0837         case 2:
0838             if (numericRecords.contains(c)) {
0839                 scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]);
0840             } else if ((c == 'D') && (m_dateFormat.isEmpty())) {
0841                 if (d->m_partPos.count() != 3) {
0842                     scanDate((*it).mid(1));
0843                     ++datesScanned;
0844                     if (d->m_partPos.count() == 2) {
0845                         // if we have detected two parts we can calculate the third and its position
0846                         d->getThirdPosition();
0847                     }
0848                 }
0849             }
0850             break;
0851         case 3:
0852             priceMatch = priceExp.match(*it);
0853             if (priceMatch.hasMatch()) {
0854                 scanNumeric(priceMatch.captured(2), m_decimal['P'], m_thousands['P']);
0855                 scanDate(priceMatch.captured(3));
0856                 ++datesScanned;
0857             }
0858             break;
0859         }
0860     }
0861 
0862     // the following algorithm is only applied if we have more
0863     // than 20 dates found. Smaller numbers have shown that the
0864     // results are inaccurate which leads to a reduced number of
0865     // date formats presented to choose from.
0866     if (d->m_partPos.count() != 3 && datesScanned > 20) {
0867         QMap<int, int> sortedPos;
0868         // make sure to reset the known parts for the following algorithm
0869         if (d->m_partPos.contains('y')) {
0870             d->m_changeCount[d->m_partPos['y']] = -1;
0871             for (int i = 0; i < 3; ++i) {
0872                 if (d->m_partPos['y'] == i)
0873                     continue;
0874                 // can we say for sure that we hit the day field?
0875                 if (d->m_largestValue[i] > 12) {
0876                     d->m_partPos['d'] = i;
0877                 }
0878             }
0879         }
0880         if (d->m_partPos.contains('d'))
0881             d->m_changeCount[d->m_partPos['d']] = -1;
0882         if (d->m_partPos.contains('m'))
0883             d->m_changeCount[d->m_partPos['m']] = -1;
0884 
0885         for (int i = 0; i < 3; ++i) {
0886             if (d->m_changeCount[i] != -1) {
0887                 sortedPos[d->m_changeCount[i]] = i;
0888             }
0889         }
0890 
0891         QMap<int, int>::const_iterator it_a;
0892         QMap<int, int>::const_iterator it_b;
0893         switch (sortedPos.count()) {
0894         case 1: // all the same
0895             // let the user decide, we can't figure it out
0896             break;
0897 
0898         case 2: // two are the same, we treat the largest as the day
0899                 // if it's 20% larger than the other one and let the
0900                 // user pick the other two
0901         {
0902             it_b = sortedPos.constBegin();
0903             it_a = it_b;
0904             ++it_b;
0905             double a = d->m_changeCount[*it_a];
0906             double b = d->m_changeCount[*it_b];
0907             if (b > (a * 1.2)) {
0908                 d->m_partPos['d'] = *it_b;
0909             }
0910         } break;
0911 
0912         case 3: // three different, we check if they are 20% apart each
0913             it_b = sortedPos.constBegin();
0914             for (int i = 0; i < 2; ++i) {
0915                 it_a = it_b;
0916                 ++it_b;
0917                 double a = d->m_changeCount[*it_a];
0918                 double b = d->m_changeCount[*it_b];
0919                 if (b > (a * 1.2)) {
0920                     switch (i) {
0921                     case 0:
0922                         d->m_partPos['y'] = *it_a;
0923                         break;
0924                     case 1:
0925                         d->m_partPos['d'] = *it_b;
0926                         break;
0927                     }
0928                 }
0929             }
0930             break;
0931         }
0932         // extract the last if necessary and possible date position
0933         d->getThirdPosition();
0934     }
0935 }
0936 
0937 void MyMoneyQifProfile::scanNumeric(const QString& txt, QChar& decimal, QChar& thousands) const
0938 {
0939     QChar first, second;
0940     static const QRegularExpression numericCharsExp(QLatin1String("[0-9-()]"));
0941     for (int i = 0; i < txt.length(); ++i) {
0942         const QChar& c = txt[i];
0943         const auto numericChars(numericCharsExp.match(c));
0944         if (!numericChars.hasMatch()) {
0945             if (c == '.' || c == ',') {
0946                 first = second;
0947                 second = c;
0948             }
0949         }
0950     }
0951     if (!second.isNull())
0952         decimal = second;
0953     if (!first.isNull())
0954         thousands = first;
0955 }
0956 
0957 void MyMoneyQifProfile::scanDate(const QString& txt) const
0958 {
0959     // extract the parts from the txt
0960     QVector<QString> parts(3); // the various parts of the date
0961     d->dissectDate(parts, txt);
0962 
0963     // now analyze the parts
0964     for (int i = 0; i < 3; ++i) {
0965         bool ok;
0966         int value = parts[i].toInt(&ok);
0967         if (!ok) { // this should happen only if the part is non-numeric -> month
0968             d->m_partPos['m'] = i;
0969         } else if (value != 0) {
0970             if (value != d->m_lastValue[i]) {
0971                 d->m_changeCount[i]++;
0972                 d->m_lastValue[i] = value;
0973                 if (value > d->m_largestValue[i])
0974                     d->m_largestValue[i] = value;
0975             }
0976             // if it's > 31 it can only be years
0977             if (value > 31) {
0978                 d->m_partPos['y'] = i;
0979             }
0980             // and if it's in between 12 and 32 and we already identified the
0981             // position for the year it must be days
0982             if ((value > 12) && (value < 32) && d->m_partPos.contains('y')) {
0983                 d->m_partPos['d'] = i;
0984             }
0985         }
0986     }
0987 }