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 }