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