1 /***************************************************************************
2                           mymoneyqifprofile.cpp  -  description
3                              -------------------
4     begin                : Tue Dec 24 2002
5     copyright            : (C) 2002 by Thomas Baumgart
6     email                : thb@net-bembel.de
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "mymoneyqifprofile.h"
19 
20 // ----------------------------------------------------------------------------
21 // QT Includes
22 
23 #include <QList>
24 #include <QRegExp>
25 #include <QVector>
26 #include <QLocale>
27 
28 // ----------------------------------------------------------------------------
29 // KDE Includes
30 
31 #include <KLocalizedString>
32 #include <KConfigGroup>
33 #include <KSharedConfig>
34 
35 // ----------------------------------------------------------------------------
36 // Project Includes
37 
38 #include "mymoneyexception.h"
39 #include "mymoneymoney.h"
40 #include "mymoneyenums.h"
41 
42 /*
43  * CENTURY_BREAK is used to identify the century for a two digit year
44  *
45  * if yr is < CENTURY_BREAK it is in 2000
46  * if yr is >= CENTURY_BREAK it is in 1900
47  *
48  * so with CENTURY_BREAK being 70 the following will happen:
49  *
50  *  00..69  ->  2000..2069
51  *  70..99  ->  1970..1999
52  */
53 const int CENTURY_BREAK = 70;
54 
55 class MyMoneyQifProfile::Private
56 {
57 public:
Private()58   Private() :
59       m_changeCount(3, 0),
60       m_lastValue(3, 0),
61       m_largestValue(3, 0) { }
62 
63   void getThirdPosition();
64   void dissectDate(QVector<QString>& parts, const QString& txt) const;
65 
66   QVector<int>    m_changeCount;
67   QVector<int>    m_lastValue;
68   QVector<int>    m_largestValue;
69   QMap<QChar, int>     m_partPos;
70 };
71 
dissectDate(QVector<QString> & parts,const QString & txt) const72 void MyMoneyQifProfile::Private::dissectDate(QVector<QString>& parts, const QString& txt) const
73 {
74   QRegExp nonDelimChars("[ 0-9a-zA-Z]");
75   int part = 0;                 // the current part we scan
76   int pos;             // the current scan position
77   int maxPartSize = txt.length() > 6 ? 4 : 2;
78   // the maximum size of a part
79   // some fu... up MS-Money versions write two delimiter in a row
80   // so we need to keep track of them. Example: D14/12/'08
81   bool lastWasDelim = false;
82 
83   // separate the parts of the date and keep the locations of the delimiters
84   for (pos = 0; pos < txt.length() && part < 3; ++pos) {
85     if (nonDelimChars.indexIn(txt[pos]) == -1) {
86       if (!lastWasDelim) {
87         ++part;
88         maxPartSize = 0;         // make sure to pick the right one depending if next char is numeric or not
89         lastWasDelim = true;
90       }
91     } else {
92       lastWasDelim = false;
93       // check if the part is over and we did not see a delimiter
94       if ((maxPartSize != 0) && (parts[part].length() == maxPartSize)) {
95         ++part;
96         maxPartSize = 0;
97       }
98       if (maxPartSize == 0) {
99         maxPartSize = txt[pos].isDigit() ? 2 : 3;
100         if (part == 2)
101           maxPartSize = 4;
102       }
103       if (part < 3)
104         parts[part] += txt[pos];
105     }
106   }
107 
108   if (part == 3) { // invalid date
109     for (int i = 0; i < 3; ++i) {
110       parts[i] = '0';
111     }
112   }
113 }
114 
115 
getThirdPosition()116 void MyMoneyQifProfile::Private::getThirdPosition()
117 {
118   // if we have detected two parts we can calculate the third and its position
119   if (m_partPos.count() == 2) {
120     QList<QChar> partsPresent = m_partPos.keys();
121     QStringList partsAvail = QString("d,m,y").split(',');
122     int missingIndex = -1;
123     int value = 0;
124     for (int i = 0; i < 3; ++i) {
125       if (!partsPresent.contains(partsAvail[i][0])) {
126         missingIndex = i;
127       } else {
128         value += m_partPos[partsAvail[i][0]];
129       }
130     }
131     m_partPos[partsAvail[missingIndex][0]] = 3 - value;
132   }
133 }
134 
135 
136 
MyMoneyQifProfile()137 MyMoneyQifProfile::MyMoneyQifProfile() :
138     d(new Private),
139     m_isDirty(false)
140 {
141   clear();
142 }
143 
MyMoneyQifProfile(const QString & name)144 MyMoneyQifProfile::MyMoneyQifProfile(const QString& name) :
145     d(new Private),
146     m_isDirty(false)
147 {
148   loadProfile(name);
149 }
150 
~MyMoneyQifProfile()151 MyMoneyQifProfile::~MyMoneyQifProfile()
152 {
153   delete d;
154 }
155 
clear()156 void MyMoneyQifProfile::clear()
157 {
158   m_dateFormat = "%d.%m.%yyyy";
159   m_apostropheFormat = "2000-2099";
160   m_valueMode = "";
161   m_filterScriptImport = "";
162   m_filterScriptExport = "";
163   m_filterFileType = "*.qif *.QIF";
164 
165   m_decimal.clear();
166   m_decimal['$'] =
167     m_decimal['Q'] =
168       m_decimal['T'] =
169         m_decimal['O'] =
170           m_decimal['I'] = QLocale().decimalPoint();
171 
172   m_thousands.clear();
173   m_thousands['$'] =
174     m_thousands['Q'] =
175       m_thousands['T'] =
176         m_thousands['O'] =
177           m_thousands['I'] = QLocale().groupSeparator();
178 
179   m_openingBalanceText = "Opening Balance";
180   m_voidMark = "VOID ";
181   m_accountDelimiter = '[';
182 
183   m_profileName = "";
184   m_profileDescription = "";
185   m_profileType = "Bank";
186 
187   m_attemptMatchDuplicates = true;
188 }
189 
loadProfile(const QString & name)190 void MyMoneyQifProfile::loadProfile(const QString& name)
191 {
192   KSharedConfigPtr config = KSharedConfig::openConfig();
193   KConfigGroup grp = config->group(name);
194 
195   clear();
196 
197   m_profileName = name;
198   m_profileDescription = grp.readEntry("Description", m_profileDescription);
199   m_profileType = grp.readEntry("Type", m_profileType);
200   m_dateFormat = grp.readEntry("DateFormat", m_dateFormat);
201   m_apostropheFormat = grp.readEntry("ApostropheFormat", m_apostropheFormat);
202   m_accountDelimiter = grp.readEntry("AccountDelimiter", m_accountDelimiter);
203   m_openingBalanceText = grp.readEntry("OpeningBalance", m_openingBalanceText);
204   m_voidMark = grp.readEntry("VoidMark", m_voidMark);
205   m_filterScriptImport = grp.readEntry("FilterScriptImport", m_filterScriptImport);
206   m_filterScriptExport = grp.readEntry("FilterScriptExport", m_filterScriptExport);
207   m_filterFileType = grp.readEntry("FilterFileType", m_filterFileType);
208 
209   m_attemptMatchDuplicates = grp.readEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates);
210 
211   // make sure, we remove any old stuff for now
212   grp.deleteEntry("FilterScript");
213 
214   QString tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] +
215                 m_decimal['$'] + m_decimal['O'];
216   tmp = grp.readEntry("Decimal", tmp);
217   m_decimal['Q'] = tmp[0];
218   m_decimal['T'] = tmp[1];
219   m_decimal['I'] = tmp[2];
220   m_decimal['$'] = tmp[3];
221   m_decimal['O'] = tmp[4];
222 
223   tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] +
224         m_thousands['$'] + m_thousands['O'];
225   tmp = grp.readEntry("Thousand", tmp);
226   m_thousands['Q'] = tmp[0];
227   m_thousands['T'] = tmp[1];
228   m_thousands['I'] = tmp[2];
229   m_thousands['$'] = tmp[3];
230   m_thousands['O'] = tmp[4];
231 
232   m_isDirty = false;
233 }
234 
saveProfile()235 void MyMoneyQifProfile::saveProfile()
236 {
237   if (m_isDirty == true) {
238     KSharedConfigPtr config = KSharedConfig::openConfig();
239     KConfigGroup grp = config->group(m_profileName);
240 
241     grp.writeEntry("Description", m_profileDescription);
242     grp.writeEntry("Type", m_profileType);
243     grp.writeEntry("DateFormat", m_dateFormat);
244     grp.writeEntry("ApostropheFormat", m_apostropheFormat);
245     grp.writeEntry("AccountDelimiter", m_accountDelimiter);
246     grp.writeEntry("OpeningBalance", m_openingBalanceText);
247     grp.writeEntry("VoidMark", m_voidMark);
248     grp.writeEntry("FilterScriptImport", m_filterScriptImport);
249     grp.writeEntry("FilterScriptExport", m_filterScriptExport);
250     grp.writeEntry("FilterFileType", m_filterFileType);
251     grp.writeEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates);
252 
253     QString tmp;
254 
255     tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] +
256           m_decimal['$'] + m_decimal['O'];
257     grp.writeEntry("Decimal", tmp);
258     tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] +
259           m_thousands['$'] + m_thousands['O'];
260     grp.writeEntry("Thousand", tmp);
261   }
262   m_isDirty = false;
263 }
264 
setProfileName(const QString & name)265 void MyMoneyQifProfile::setProfileName(const QString& name)
266 {
267   if (m_profileName != name)
268     m_isDirty = true;
269 
270   m_profileName = name;
271 }
272 
setProfileDescription(const QString & desc)273 void MyMoneyQifProfile::setProfileDescription(const QString& desc)
274 {
275   if (m_profileDescription != desc)
276     m_isDirty = true;
277 
278   m_profileDescription = desc;
279 }
280 
setProfileType(const QString & type)281 void MyMoneyQifProfile::setProfileType(const QString& type)
282 {
283   if (m_profileType != type)
284     m_isDirty = true;
285   m_profileType = type;
286 }
287 
setOutputDateFormat(const QString & dateFormat)288 void MyMoneyQifProfile::setOutputDateFormat(const QString& dateFormat)
289 {
290   if (m_dateFormat != dateFormat)
291     m_isDirty = true;
292 
293   m_dateFormat = dateFormat;
294 }
295 
setInputDateFormat(const QString & dateFormat)296 void MyMoneyQifProfile::setInputDateFormat(const QString& dateFormat)
297 {
298   int j = -1;
299   if (dateFormat.length() > 0) {
300     for (int i = 0; i < dateFormat.length() - 1; ++i) {
301       if (dateFormat[i] == '%') {
302         d->m_partPos[dateFormat[++i]] = ++j;
303       }
304     }
305   }
306 }
307 
setApostropheFormat(const QString & apostropheFormat)308 void MyMoneyQifProfile::setApostropheFormat(const QString& apostropheFormat)
309 {
310   if (m_apostropheFormat != apostropheFormat)
311     m_isDirty = true;
312 
313   m_apostropheFormat = apostropheFormat;
314 }
315 
setAmountDecimal(const QChar & def,const QChar & chr)316 void MyMoneyQifProfile::setAmountDecimal(const QChar& def, const QChar& chr)
317 {
318   QChar ch(chr);
319   if (ch == QChar())
320     ch = ' ';
321 
322   if (m_decimal[def] != ch)
323     m_isDirty = true;
324 
325   m_decimal[def] = ch;
326 }
327 
setAmountThousands(const QChar & def,const QChar & chr)328 void MyMoneyQifProfile::setAmountThousands(const QChar& def, const QChar& chr)
329 {
330   QChar ch(chr);
331   if (ch == QChar())
332     ch = ' ';
333 
334   if (m_thousands[def] != ch)
335     m_isDirty = true;
336 
337   m_thousands[def] = ch;
338 }
339 
amountDecimal(const QChar & def) const340 const QChar MyMoneyQifProfile::amountDecimal(const QChar& def) const
341 {
342   QChar chr = m_decimal[def];
343   return chr;
344 }
345 
amountThousands(const QChar & def) const346 const QChar MyMoneyQifProfile::amountThousands(const QChar& def) const
347 {
348   QChar chr = m_thousands[def];
349   return chr;
350 }
351 
setAccountDelimiter(const QString & delim)352 void MyMoneyQifProfile::setAccountDelimiter(const QString& delim)
353 {
354   QString txt(delim);
355 
356   if (txt.isEmpty())
357     txt = ' ';
358   else if (txt[0] != '[')
359     txt = '[';
360 
361   if (m_accountDelimiter[0] != txt[0])
362     m_isDirty = true;
363   m_accountDelimiter = txt[0];
364 }
365 
setOpeningBalanceText(const QString & txt)366 void MyMoneyQifProfile::setOpeningBalanceText(const QString& txt)
367 {
368   if (m_openingBalanceText != txt)
369     m_isDirty = true;
370   m_openingBalanceText = txt;
371 }
372 
setVoidMark(const QString & txt)373 void MyMoneyQifProfile::setVoidMark(const QString& txt)
374 {
375   if (m_voidMark != txt)
376     m_isDirty = true;
377   m_voidMark = txt;
378 }
379 
accountDelimiter() const380 const QString MyMoneyQifProfile::accountDelimiter() const
381 {
382   QString rc;
383   if (m_accountDelimiter[0] == ' ')
384     rc = ' ';
385   else
386     rc = "[]";
387   return rc;
388 }
389 
date(const QDate & datein) const390 const QString MyMoneyQifProfile::date(const QDate& datein) const
391 {
392   QString::const_iterator format = m_dateFormat.begin();;
393   QString buffer;
394   QChar delim;
395   int maskLen;
396   QChar maskChar;
397 
398   while (format != m_dateFormat.end()) {
399     if (*format == '%') {
400       maskLen = 0;
401       maskChar = *(++format);
402       while ((format != m_dateFormat.end()) && (*format == maskChar)) {
403         ++maskLen;
404         ++format;
405       }
406 
407       if (maskChar == 'd') {
408         if (! delim.isNull())
409           buffer += delim;
410         buffer += QString::number(datein.day()).rightJustified(2, '0');
411 
412       } else if (maskChar == 'm') {
413         if (! delim.isNull())
414           buffer += delim;
415         if (maskLen == 3)
416           buffer += QLocale().monthName(datein.month(), QLocale::ShortFormat);
417         else
418           buffer += QString::number(datein.month()).rightJustified(2, '0');
419       } else if (maskChar == 'y') {
420         if (maskLen == 2) {
421           buffer += twoDigitYear(delim, datein.year());
422         } else {
423           if (! delim.isNull())
424             buffer += delim;
425           buffer += QString::number(datein.year());
426         }
427         break;
428       } else {
429         throw MYMONEYEXCEPTION_CSTRING("Invalid char in QifProfile date field");
430       }
431       delim = 0;
432     } else {
433       if (! delim.isNull())
434         buffer += delim;
435       delim = *format++;
436     }
437   }
438   return buffer;
439 }
440 
date(const QString & datein) const441 const QDate MyMoneyQifProfile::date(const QString& datein) const
442 {
443   // in case we don't know the format, we return an invalid date
444   if (d->m_partPos.count() != 3)
445     return QDate();
446 
447   QVector<QString> scannedParts(3);
448   d->dissectDate(scannedParts, datein);
449 
450   int yr, mon, day;
451   bool ok;
452   yr = scannedParts[d->m_partPos['y']].toInt();
453   mon = scannedParts[d->m_partPos['m']].toInt(&ok);
454   if (!ok) {
455     QStringList monthNames = QString("jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec").split(',');
456     int j;
457     for (j = 1; j <= 12; ++j) {
458       if ((QLocale().monthName(j, QLocale::ShortFormat).toLower() == scannedParts[d->m_partPos['m']].toLower())
459           || (monthNames[j-1] == scannedParts[d->m_partPos['m']].toLower())) {
460         mon = j;
461         break;
462       }
463     }
464     if (j == 13) {
465       qWarning("Unknown month '%s'", qPrintable(scannedParts[d->m_partPos['m']]));
466       return QDate();
467     }
468   }
469 
470   day = scannedParts[d->m_partPos['d']].toInt();
471   if (yr < 100) { // two digit year information?
472     if (yr < CENTURY_BREAK)  // less than the CENTURY_BREAK we assume this century
473       yr += 2000;
474     else
475       yr += 1900;
476   }
477   return QDate(yr, mon, day);
478 
479 #if 0
480   QString scannedDelim[2];
481   QString formatParts[3];
482   QString formatDelim[2];
483   int part;
484   int delim;
485   unsigned int i, j;
486 
487   part = -1;
488   delim = 0;
489   for (i = 0; i < m_dateFormat.length(); ++i) {
490     if (m_dateFormat[i] == '%') {
491       ++part;
492       if (part == 3) {
493         qWarning("MyMoneyQifProfile::date(const QString& datein) Too many parts in date format");
494         return QDate();
495       }
496       ++i;
497     }
498     switch (m_dateFormat[i].toLatin1()) {
499       case 'm':
500       case 'd':
501       case 'y':
502         formatParts[part] += m_dateFormat[i];
503         break;
504       case '/':
505       case '-':
506       case '.':
507       case '\'':
508         if (delim == 2) {
509           qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date format");
510           return QDate();
511         }
512         formatDelim[delim] = m_dateFormat[i];
513         ++delim;
514         break;
515       default:
516         qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid char in date format");
517         return QDate();
518     }
519   }
520 
521 
522   part = 0;
523   delim = 0;
524   bool prevWasChar = false;
525   for (i = 0; i < datein.length(); ++i) {
526     switch (datein[i].toLatin1()) {
527       case '/':
528       case '.':
529       case '-':
530       case '\'':
531         if (delim == 2) {
532           qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date field");
533           return QDate();
534         }
535         scannedDelim[delim] = datein[i];
536         ++delim;
537         ++part;
538         prevWasChar = false;
539         break;
540 
541       default:
542         if (prevWasChar && datein[i].isDigit()) {
543           ++part;
544           prevWasChar = false;
545         }
546         if (datein[i].isLetter())
547           prevWasChar = true;
548         // replace blank with 0
549         scannedParts[part] += (datein[i] == ' ') ? QChar('0') : datein[i];
550         break;
551     }
552   }
553 
554   int day = 1,
555             mon = 1,
556                   yr = 1900;
557   bool ok = false;
558   for (i = 0; i < 2; ++i) {
559     if (scannedDelim[i] != formatDelim[i]
560         && scannedDelim[i] != QChar('\'')) {
561       qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid delimiter '%s' when '%s' was expected",
562                scannedDelim[i].toLatin1(), formatDelim[i].toLatin1());
563       return QDate();
564     }
565   }
566 
567   QString msg;
568   for (i = 0; i < 3; ++i) {
569     switch (formatParts[i][0].toLatin1()) {
570       case 'd':
571         day = scannedParts[i].toUInt(&ok);
572         if (!ok)
573           msg = "Invalid numeric character in day string";
574         break;
575       case 'm':
576         if (formatParts[i].length() != 3) {
577           mon = scannedParts[i].toUInt(&ok);
578           if (!ok)
579             msg = "Invalid numeric character in month string";
580         } else {
581           for (j = 1; j <= 12; ++j) {
582             if (QLocale().monthName(j, 2000, true).toLower() == formatParts[i].toLower()) {
583               mon = j;
584               ok = true;
585               break;
586             }
587           }
588           if (j == 13) {
589             msg = "Unknown month '" + scannedParts[i] + "'";
590           }
591         }
592         break;
593       case 'y':
594         ok = false;
595         if (scannedParts[i].length() == formatParts[i].length()) {
596           yr = scannedParts[i].toUInt(&ok);
597           if (!ok)
598             msg = "Invalid numeric character in month string";
599           if (yr < 100) {     // two digit year info
600             if (i > 1) {
601               ok = true;
602               if (scannedDelim[i-1] == QChar('\'')) {
603                 if (m_apostropheFormat == "1900-1949") {
604                   if (yr < 50)
605                     yr += 1900;
606                   else
607                     yr += 2000;
608                 } else if (m_apostropheFormat == "1900-1999") {
609                   yr += 1900;
610                 } else if (m_apostropheFormat == "2000-2099") {
611                   yr += 2000;
612                 } else {
613                   msg = "Unsupported apostropheFormat!";
614                   ok = false;
615                 }
616               } else {
617                 if (m_apostropheFormat == "1900-1949") {
618                   if (yr < 50)
619                     yr += 2000;
620                   else
621                     yr += 1900;
622                 } else if (m_apostropheFormat == "1900-1999") {
623                   yr += 2000;
624                 } else if (m_apostropheFormat == "2000-2099") {
625                   yr += 1900;
626                 } else {
627                   msg = "Unsupported apostropheFormat!";
628                   ok = false;
629                 }
630               }
631             } else {
632               msg = "Year as first parameter is not supported!";
633             }
634           } else if (yr < 1900) {
635             msg = "Year not in range < 100 or >= 1900!";
636           } else {
637             ok = true;
638           }
639         } else {
640           msg = QString("Length of year (%1) does not match expected length (%2).")
641                 .arg(scannedParts[i].length()).arg(formatParts[i].length());
642         }
643         break;
644     }
645     if (!msg.isEmpty()) {
646       qWarning("MyMoneyQifProfile::date(const QString& datein) %s", msg.toLatin1());
647       return QDate();
648     }
649   }
650   return QDate(yr, mon, day);
651 #endif
652 }
653 
twoDigitYear(const QChar & delim,int yr) const654 const QString MyMoneyQifProfile::twoDigitYear(const QChar& delim, int yr) const
655 {
656   QChar realDelim = delim;
657   QString buffer;
658   if (!delim.isNull()) {
659     if ((m_apostropheFormat == "1900-1949" && yr <= 1949)
660         || (m_apostropheFormat == "1900-1999" && yr <= 1999)
661         || (m_apostropheFormat == "2000-2099" && yr >= 2000))
662       realDelim = '\'';
663     buffer += realDelim;
664   }
665   yr -= 1900;
666   if (yr > 100)
667     yr -= 100;
668 
669   if (yr < 10)
670     buffer += '0';
671 
672   buffer += QString::number(yr);
673   return buffer;
674 }
675 
value(const QChar & def,const MyMoneyMoney & valuein) const676 const QString MyMoneyQifProfile::value(const QChar& def, const MyMoneyMoney& valuein) const
677 {
678   QChar _decimalSeparator;
679   QChar _thousandsSeparator;
680   QString res;
681 
682   _decimalSeparator = MyMoneyMoney::decimalSeparator();
683   _thousandsSeparator = MyMoneyMoney::thousandSeparator();
684   eMyMoney::Money::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition();
685 
686   MyMoneyMoney::setDecimalSeparator(amountDecimal(def).toLatin1());
687   MyMoneyMoney::setThousandSeparator(amountThousands(def).toLatin1());
688   MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::PreceedQuantityAndSymbol);
689   res = valuein.formatMoney("", 2);
690 
691   MyMoneyMoney::setDecimalSeparator(_decimalSeparator);
692   MyMoneyMoney::setThousandSeparator(_thousandsSeparator);
693   MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition);
694 
695   return res;
696 }
697 
value(const QChar & def,const QString & valuein) const698 const MyMoneyMoney MyMoneyQifProfile::value(const QChar& def, const QString& valuein) const
699 {
700   QChar _decimalSeparator;
701   QChar _thousandsSeparator;
702   MyMoneyMoney res;
703 
704   _decimalSeparator = MyMoneyMoney::decimalSeparator();
705   _thousandsSeparator = MyMoneyMoney::thousandSeparator();
706   eMyMoney::Money::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition();
707 
708   MyMoneyMoney::setDecimalSeparator(amountDecimal(def).toLatin1());
709   MyMoneyMoney::setThousandSeparator(amountThousands(def).toLatin1());
710   MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::PreceedQuantityAndSymbol);
711 
712   res = MyMoneyMoney(valuein);
713 
714   MyMoneyMoney::setDecimalSeparator(_decimalSeparator);
715   MyMoneyMoney::setThousandSeparator(_thousandsSeparator);
716   MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition);
717 
718   return res;
719 }
720 
setFilterScriptImport(const QString & script)721 void MyMoneyQifProfile::setFilterScriptImport(const QString& script)
722 {
723   if (m_filterScriptImport != script)
724     m_isDirty = true;
725 
726   m_filterScriptImport = script;
727 }
728 
setFilterScriptExport(const QString & script)729 void MyMoneyQifProfile::setFilterScriptExport(const QString& script)
730 {
731   if (m_filterScriptExport != script)
732     m_isDirty = true;
733 
734   m_filterScriptExport = script;
735 }
736 
setFilterFileType(const QString & txt)737 void MyMoneyQifProfile::setFilterFileType(const QString& txt)
738 {
739   if (m_filterFileType != txt)
740     m_isDirty = true;
741 
742   m_filterFileType = txt;
743 }
744 
setAttemptMatchDuplicates(bool f)745 void MyMoneyQifProfile::setAttemptMatchDuplicates(bool f)
746 {
747   if (m_attemptMatchDuplicates != f)
748     m_isDirty = true;
749 
750   m_attemptMatchDuplicates = f;
751 }
752 
inputDateFormat() const753 const QString MyMoneyQifProfile::inputDateFormat() const
754 {
755   QStringList list;
756   possibleDateFormats(list);
757   if (list.count() == 1)
758     return list.first();
759   return QString();
760 }
761 
possibleDateFormats(QStringList & list) const762 void MyMoneyQifProfile::possibleDateFormats(QStringList& list) const
763 {
764   QStringList defaultList = QString("y,m,d:y,d,m:m,d,y:m,y,d:d,m,y:d,y,m").split(':');
765   list.clear();
766   QStringList::const_iterator it_d;
767   for (it_d = defaultList.constBegin(); it_d != defaultList.constEnd(); ++it_d) {
768     const QStringList parts = (*it_d).split(',', QString::SkipEmptyParts);
769     int i;
770     for (i = 0; i < 3; ++i) {
771       if (d->m_partPos.contains(parts[i][0])) {
772         if (d->m_partPos[parts[i][0]] != i)
773           break;
774       }
775       // months can't be larger than 12
776       if (parts[i] == "m" && d->m_largestValue[i] > 12)
777         break;
778       // days can't be larger than 31
779       if (parts[i] == "d" && d->m_largestValue[i] > 31)
780         break;
781     }
782     // matches all tests
783     if (i == 3) {
784       QString format = *it_d;
785       format.replace('y', "%y");
786       format.replace('m', "%m");
787       format.replace('d', "%d");
788       format.replace(',', " ");
789       list << format;
790     }
791   }
792   // if we haven't found any, then there's something wrong.
793   // in this case, we present the full list and let the user decide
794   if (list.count() == 0) {
795     for (it_d = defaultList.constBegin(); it_d != defaultList.constEnd(); ++it_d) {
796       QString format = *it_d;
797       format.replace('y', "%y");
798       format.replace('m', "%m");
799       format.replace('d', "%d");
800       format.replace(',', " ");
801       list << format;
802     }
803   }
804 }
805 
autoDetect(const QStringList & lines)806 void MyMoneyQifProfile::autoDetect(const QStringList& lines)
807 {
808   m_dateFormat.clear();
809   m_decimal.clear();
810   m_thousands.clear();
811 
812   QString numericRecords = "BT$OIQ";
813   QStringList::const_iterator it;
814   int datesScanned = 0;
815   // section: used to switch between different QIF sections,
816   // because the Record identifiers are ambiguous between sections
817   // eg. in transaction records, T identifies a total amount, in
818   // account sections it's the type.
819   //
820   // 0 - unknown
821   // 1 - account
822   // 2 - transactions
823   // 3 - prices
824   int section = 0;
825   QRegExp price("\"(.*)\",(.*),\"(.*)\"");
826   for (it = lines.begin(); it != lines.end(); ++it) {
827     QChar c((*it)[0]);
828     if (c == '!') {
829       QString sname = (*it).toLower();
830       if (!sname.startsWith(QLatin1String("!option:"))) {
831         section = 0;
832         if (sname.startsWith(QLatin1String("!account")))
833           section = 1;
834         else if (sname.startsWith(QLatin1String("!type"))) {
835           if (sname.startsWith(QLatin1String("!type:cat"))
836               || sname.startsWith(QLatin1String("!type:payee"))
837               || sname.startsWith(QLatin1String("!type:security"))
838               || sname.startsWith(QLatin1String("!type:class"))) {
839             section = 0;
840           } else if (sname.startsWith(QLatin1String("!type:price"))) {
841             section = 3;
842           } else
843             section = 2;
844         }
845       }
846     }
847 
848     switch (section) {
849       case 1:
850         if (c == 'B') {
851           scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]);
852         }
853         break;
854       case 2:
855         if (numericRecords.contains(c)) {
856           scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]);
857         } else if ((c == 'D') && (m_dateFormat.isEmpty())) {
858           if (d->m_partPos.count() != 3) {
859             scanDate((*it).mid(1));
860             ++datesScanned;
861             if (d->m_partPos.count() == 2) {
862               // if we have detected two parts we can calculate the third and its position
863               d->getThirdPosition();
864             }
865           }
866         }
867         break;
868       case 3:
869         if (price.indexIn(*it) != -1) {
870           scanNumeric(price.cap(2), m_decimal['P'], m_thousands['P']);
871           scanDate(price.cap(3));
872           ++datesScanned;
873         }
874         break;
875     }
876   }
877 
878   // the following algorithm is only applied if we have more
879   // than 20 dates found. Smaller numbers have shown that the
880   // results are inaccurate which leads to a reduced number of
881   // date formats presented to choose from.
882   if (d->m_partPos.count() != 3 && datesScanned > 20) {
883     QMap<int, int> sortedPos;
884     // make sure to reset the known parts for the following algorithm
885     if (d->m_partPos.contains('y')) {
886       d->m_changeCount[d->m_partPos['y']] = -1;
887       for (int i = 0; i < 3; ++i) {
888         if (d->m_partPos['y'] == i)
889           continue;
890         // can we say for sure that we hit the day field?
891         if (d->m_largestValue[i] > 12) {
892           d->m_partPos['d'] = i;
893         }
894       }
895     }
896     if (d->m_partPos.contains('d'))
897       d->m_changeCount[d->m_partPos['d']] = -1;
898     if (d->m_partPos.contains('m'))
899       d->m_changeCount[d->m_partPos['m']] = -1;
900 
901     for (int i = 0; i < 3; ++i) {
902       if (d->m_changeCount[i] != -1) {
903         sortedPos[d->m_changeCount[i]] = i;
904       }
905     }
906 
907     QMap<int, int>::const_iterator it_a;
908     QMap<int, int>::const_iterator it_b;
909     switch (sortedPos.count()) {
910       case 1: // all the same
911         // let the user decide, we can't figure it out
912         break;
913 
914       case 2: // two are the same, we treat the largest as the day
915         // if it's 20% larger than the other one and let the
916         // user pick the other two
917         {
918           it_b = sortedPos.constBegin();
919           it_a = it_b;
920           ++it_b;
921           double a = d->m_changeCount[*it_a];
922           double b = d->m_changeCount[*it_b];
923           if (b > (a * 1.2)) {
924             d->m_partPos['d'] = *it_b;
925           }
926         }
927         break;
928 
929       case 3: // three different, we check if they are 20% apart each
930         it_b = sortedPos.constBegin();
931         for (int i = 0; i < 2; ++i) {
932           it_a = it_b;
933           ++it_b;
934           double a = d->m_changeCount[*it_a];
935           double b = d->m_changeCount[*it_b];
936           if (b > (a * 1.2)) {
937             switch (i) {
938               case 0:
939                 d->m_partPos['y'] = *it_a;
940                 break;
941               case 1:
942                 d->m_partPos['d'] = *it_b;
943                 break;
944             }
945           }
946         }
947         break;
948     }
949     // extract the last if necessary and possible date position
950     d->getThirdPosition();
951   }
952 }
953 
scanNumeric(const QString & txt,QChar & decimal,QChar & thousands) const954 void MyMoneyQifProfile::scanNumeric(const QString& txt, QChar& decimal, QChar& thousands) const
955 {
956   QChar first, second;
957   QRegExp numericChars("[0-9-()]");
958   for (int i = 0; i < txt.length(); ++i) {
959     const QChar& c = txt[i];
960     if (numericChars.indexIn(c) == -1) {
961       if (c == '.' || c == ',') {
962         first = second;
963         second = c;
964       }
965     }
966   }
967   if (!second.isNull())
968     decimal = second;
969   if (!first.isNull())
970     thousands = first;
971 }
972 
scanDate(const QString & txt) const973 void MyMoneyQifProfile::scanDate(const QString& txt) const
974 {
975   // extract the parts from the txt
976   QVector<QString> parts(3);             // the various parts of the date
977   d->dissectDate(parts, txt);
978 
979   // now analyze the parts
980   for (int i = 0; i < 3; ++i) {
981     bool ok;
982     int value = parts[i].toInt(&ok);
983     if (!ok) { // this should happen only if the part is non-numeric -> month
984       d->m_partPos['m'] = i;
985     } else if (value != 0) {
986       if (value != d->m_lastValue[i]) {
987         d->m_changeCount[i]++;
988         d->m_lastValue[i] = value;
989         if (value > d->m_largestValue[i])
990           d->m_largestValue[i] = value;
991       }
992       // if it's > 31 it can only be years
993       if (value > 31) {
994         d->m_partPos['y'] = i;
995       }
996       // and if it's in between 12 and 32 and we already identified the
997       // position for the year it must be days
998       if ((value > 12) && (value < 32) && d->m_partPos.contains('y')) {
999         d->m_partPos['d'] = i;
1000       }
1001     }
1002   }
1003 }
1004