File indexing completed on 2024-05-19 05:06:56

0001 /*
0002     SPDX-FileCopyrightText: 2004 Ace Jones <acejones@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "webpricequote.h"
0008 
0009 // ----------------------------------------------------------------------------
0010 // QT Headers
0011 
0012 #include <QFile>
0013 #include <QTextCodec>
0014 #include <QByteArray>
0015 #include <QString>
0016 #include <QTemporaryFile>
0017 #include <QUrl>
0018 #include <QRegularExpression>
0019 #include <QDebug>
0020 #include <QLoggingCategory>
0021 #include <QLocale>
0022 
0023 // ----------------------------------------------------------------------------
0024 // KDE Headers
0025 
0026 #include <KLocalizedString>
0027 #include <KConfig>
0028 #include <KShell>
0029 #include <KConfigGroup>
0030 #include <KEncodingProber>
0031 #include <KIO/Job>
0032 #include <KJobWidgets>
0033 
0034 // ----------------------------------------------------------------------------
0035 // Project Headers
0036 
0037 #include "mymoneyexception.h"
0038 #include "mymoneyfile.h"
0039 #include "mymoneysecurity.h"
0040 
0041 #include <alkimia/alkenvironment.h>
0042 
0043 Q_DECLARE_LOGGING_CATEGORY(WEBPRICEQUOTE)
0044 Q_LOGGING_CATEGORY(WEBPRICEQUOTE, "kmymoney_webpricequote")
0045 
0046 // define static members
0047 QString WebPriceQuote::m_financeQuoteScriptPath;
0048 QStringList WebPriceQuote::m_financeQuoteSources;
0049 
0050 class WebPriceQuote::Private
0051 {
0052 public:
0053     WebPriceQuoteProcess m_filter;
0054     QString m_webID;
0055     QString m_kmmID;
0056     QDate m_date;
0057     QDate m_fromDate;
0058     QDate m_toDate;
0059     double m_price;
0060     WebPriceQuoteSource m_source;
0061     PricesProfile    m_CSVSource;
0062 };
0063 
0064 WebPriceQuote::WebPriceQuote(QObject* _parent):
0065     QObject(_parent),
0066     d(new Private)
0067 {
0068     // only do this once (I know, it is not thread safe, but it should
0069     // always yield the same result so we don't do any semaphore foo here)
0070     if (m_financeQuoteScriptPath.isEmpty()) {
0071         m_financeQuoteScriptPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QString("misc/financequote.pl"));
0072     }
0073     connect(&d->m_filter, &WebPriceQuoteProcess::processExited, this, &WebPriceQuote::slotParseQuote);
0074 }
0075 
0076 WebPriceQuote::~WebPriceQuote()
0077 {
0078     delete d;
0079 }
0080 
0081 void WebPriceQuote::setDate(const QDate& _from, const QDate& _to)
0082 {
0083     d->m_fromDate = _from;
0084     d->m_toDate = _to;
0085 }
0086 
0087 bool WebPriceQuote::launch(const QString& _webID, const QString& _kmmID, const QString& _sourcename)
0088 {
0089     if (_sourcename.contains("Finance::Quote"))
0090         return (launchFinanceQuote(_webID, _kmmID, _sourcename));
0091     else if ((!d->m_fromDate.isValid() || !d->m_toDate.isValid()) ||
0092              (d->m_fromDate == d->m_toDate && d->m_toDate == QDate::currentDate()))
0093         return (launchNative(_webID, _kmmID, _sourcename));
0094     else
0095         return launchCSV(_webID, _kmmID, _sourcename);
0096 }
0097 
0098 bool WebPriceQuote::launchCSV(const QString& _webID, const QString& _kmmID, const QString& _sourcename)
0099 {
0100     d->m_webID = _webID;
0101     d->m_kmmID = _kmmID;
0102 
0103 //   Q_EMIT status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0104 
0105     // Get sources from the config file
0106     QString sourcename = _sourcename;
0107     if (sourcename.isEmpty())
0108         return false;
0109 
0110     // for historical exchange rates we switch to Stooq
0111     if (sourcename == QLatin1String("KMyMoney Currency"))
0112         sourcename = QLatin1String("Stooq Currency");
0113 
0114     if (quoteSources().contains(sourcename))
0115         d->m_source = WebPriceQuoteSource(sourcename);
0116     else {
0117         Q_EMIT error(i18n("Source <placeholder>%1</placeholder> does not exist.", sourcename));
0118         Q_EMIT failed(d->m_kmmID, d->m_webID);
0119         return false;
0120     }
0121 
0122     int monthOffset = 0;
0123     if (sourcename.contains(QLatin1String("Yahoo"), Qt::CaseInsensitive))
0124         monthOffset = -1;
0125 
0126     QUrl url;
0127     QString urlStr = d->m_source.m_csvUrl;
0128     int i = urlStr.indexOf(QLatin1String("%y"));
0129     if (i != -1)
0130         urlStr.replace(i, 2, QString().setNum(d->m_fromDate.year()));
0131     i = urlStr.indexOf(QLatin1String("%y"));
0132     if (i != -1)
0133         urlStr.replace(i, 2, QString().setNum(d->m_toDate.year()));
0134 
0135     i = urlStr.indexOf(QLatin1String("%m"));
0136     if (i != -1)
0137         urlStr.replace(i, 2, QString().setNum(d->m_fromDate.month() + monthOffset).rightJustified(2, QLatin1Char('0')));
0138     i = urlStr.indexOf(QLatin1String("%m"));
0139     if (i != -1)
0140         urlStr.replace(i, 2, QString().setNum(d->m_toDate.month() + monthOffset).rightJustified(2, QLatin1Char('0')));
0141 
0142     i = urlStr.indexOf(QLatin1String("%d"));
0143     if (i != -1)
0144         urlStr.replace(i, 2, QString().setNum(d->m_fromDate.day()).rightJustified(2, QLatin1Char('0')));
0145     i = urlStr.indexOf(QLatin1String("%d"));
0146     if (i != -1)
0147         urlStr.replace(i, 2, QString().setNum(d->m_toDate.day()).rightJustified(2, QLatin1Char('0')));
0148 
0149     if (urlStr.contains(QLatin1String("%y")) || urlStr.contains(QLatin1String("%m")) || urlStr.contains(QLatin1String("%d"))) {
0150         Q_EMIT error(i18n("Cannot resolve input date."));
0151         Q_EMIT failed(d->m_kmmID, d->m_webID);
0152         return false;
0153     }
0154 
0155     bool isCurrency = false;
0156     if (urlStr.contains(QLatin1String("%2"))) {
0157         d->m_CSVSource.m_profileType = Profile::CurrencyPrices;
0158         isCurrency = true;
0159     } else
0160         d->m_CSVSource.m_profileType = Profile::StockPrices;
0161 
0162     d->m_CSVSource.m_profileName = sourcename;
0163     if (!d->m_CSVSource.readSettings(CSVImporterCore::configFile())) {
0164         QMap<QString, PricesProfile> result = defaultCSVQuoteSources();
0165         d->m_CSVSource = result.value(sourcename);
0166         if (d->m_CSVSource.m_profileName.isEmpty()) {
0167             Q_EMIT error(i18n("CSV source <placeholder>%1</placeholder> does not exist.", sourcename));
0168             Q_EMIT failed(d->m_kmmID, d->m_webID);
0169             return false;
0170         }
0171     }
0172 
0173     if (isCurrency) {
0174         // this is a two-symbol quote.  split the symbol into two.  valid symbol
0175         // characters are: 0-9, A-Z and the dot.  anything else is a separator
0176         QRegularExpression splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", QRegularExpression::CaseInsensitiveOption);
0177         QRegularExpressionMatch match;
0178         // if we've truly found 2 symbols delimited this way...
0179         if (d->m_webID.indexOf(splitrx, 0, &match) != -1) {
0180             url = QUrl(urlStr.arg(match.captured(1), match.captured(2)));
0181             d->m_CSVSource.m_currencySymbol = match.captured(2);
0182             d->m_CSVSource.m_securitySymbol = match.captured(1);
0183         } else {
0184             qCDebug(WEBPRICEQUOTE) << "WebPriceQuote::launch() did not find 2 symbols";
0185             Q_EMIT error(i18n("Cannot find from and to currency."));
0186             Q_EMIT failed(d->m_kmmID, d->m_webID);
0187             return false;
0188         }
0189 
0190     } else {
0191         // a regular one-symbol quote
0192         url = QUrl(urlStr.arg(d->m_webID));
0193         d->m_CSVSource.m_securityName = MyMoneyFile::instance()->security(d->m_kmmID).name();
0194         d->m_CSVSource.m_securitySymbol = MyMoneyFile::instance()->security(d->m_kmmID).tradingSymbol();
0195     }
0196 
0197     if (url.isLocalFile()) {
0198         Q_EMIT error(i18n("Local quote sources aren't supported."));
0199         Q_EMIT failed(d->m_kmmID, d->m_webID);
0200         return false;
0201     } else {
0202         //silent download
0203         Q_EMIT status(i18n("Fetching URL %1...", url.toDisplayString()));
0204         QString tmpFile;
0205         {
0206             QTemporaryFile tmpFileFile;
0207             tmpFileFile.setAutoRemove(false);
0208             if (tmpFileFile.open())
0209                 qDebug() << "created tmpfile";
0210 
0211             tmpFile = tmpFileFile.fileName();
0212         }
0213         QFile::remove(tmpFile);
0214         const QUrl dest = QUrl::fromLocalFile(tmpFile);
0215         KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::HideProgressInfo);
0216         connect(job, SIGNAL(result(KJob*)),
0217                 this, SLOT(downloadCSV(KJob*)));
0218     }
0219     return true;
0220 }
0221 
0222 bool WebPriceQuote::launchNative(const QString& _webID, const QString& _kmmID, const QString& _sourcename)
0223 {
0224     d->m_webID = _webID;
0225     d->m_kmmID = _kmmID;
0226 
0227     if (_webID == i18n("[No identifier]")) {
0228         Q_EMIT error(i18n("<placeholder>%1</placeholder> skipped because it doesn't have identification number.", _kmmID));
0229         Q_EMIT failed(d->m_kmmID, d->m_webID);
0230         return false;
0231     }
0232 //   Q_EMIT status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0233 
0234     // Get sources from the config file
0235     QString sourcename = _sourcename;
0236     if (sourcename.isEmpty())
0237         sourcename = "Yahoo";
0238 
0239     if (quoteSources().contains(sourcename))
0240         d->m_source = WebPriceQuoteSource(sourcename);
0241     else {
0242         Q_EMIT error(i18n("Source <placeholder>%1</placeholder> does not exist.", sourcename));
0243         Q_EMIT failed(d->m_kmmID, d->m_webID);
0244         return false;
0245     }
0246 
0247     QUrl url;
0248 
0249     // if the source has room for TWO symbols..
0250     if (d->m_source.m_url.contains("%2")) {
0251         // this is a two-symbol quote.  split the symbol into two.  valid symbol
0252         // characters are: 0-9, A-Z and the dot.  anything else is a separator
0253         QRegularExpression splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", QRegularExpression::CaseInsensitiveOption);
0254         QRegularExpressionMatch match;
0255         // if we've truly found 2 symbols delimited this way...
0256         if (d->m_webID.indexOf(splitrx, 0, &match) != -1) {
0257             url = QUrl(d->m_source.m_url.arg(match.captured(1), match.captured(2)));
0258         } else {
0259             qCDebug(WEBPRICEQUOTE) << "WebPriceQuote::launch() did not find 2 symbols";
0260         }
0261     } else {
0262         // a regular one-symbol quote
0263         url = QUrl(d->m_source.m_url.arg(d->m_webID));
0264     }
0265 
0266     if (url.isLocalFile()) {
0267         Q_EMIT status(i18nc("The process x is executing", "Executing %1...", url.toLocalFile()));
0268 
0269         QString program;
0270         QStringList arguments = url.toLocalFile().split(' ', Qt::SkipEmptyParts);
0271         if (!arguments.isEmpty()) {
0272             program = arguments.first();
0273             arguments.removeFirst();
0274 
0275             // in case we are running as AppImage, we need
0276             // to prepend "/bin/sh -c " here
0277             if (qEnvironmentVariableIsSet("APPDIR")) {
0278                 program = QStringLiteral("/bin/sh");
0279                 arguments.clear();
0280                 arguments << QStringLiteral("-c");
0281                 arguments << url.toLocalFile();
0282 
0283                 // make sure to fix the LD_LIBRARY_PATH
0284                 // to not include APPDIR subdirectories
0285                 AlkEnvironment::removeAppImagePathFromLinkLoaderLibPath(&d->m_filter);
0286             }
0287         }
0288         d->m_filter.setWebID(d->m_webID);
0289 
0290         d->m_filter.setProcessChannelMode(QProcess::SeparateChannels);
0291         d->m_filter.start(program, arguments);
0292 
0293         if (!d->m_filter.waitForStarted()) {
0294             Q_EMIT error(i18n("Unable to launch: %1", url.toLocalFile()));
0295             slotParseQuote(QString(), QString());
0296         }
0297     } else {
0298         //silent download
0299         Q_EMIT status(i18n("Fetching URL %1...", url.toDisplayString()));
0300         QString tmpFile;
0301         {
0302             QTemporaryFile tmpFileFile;
0303             tmpFileFile.setAutoRemove(false);
0304             if (tmpFileFile.open())
0305                 qDebug() << "created tmpfile";
0306 
0307             tmpFile = tmpFileFile.fileName();
0308         }
0309         QFile::remove(tmpFile);
0310         const QUrl dest = QUrl::fromLocalFile(tmpFile);
0311         KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::HideProgressInfo);
0312         connect(job, SIGNAL(result(KJob*)),
0313                 this, SLOT(downloadResult(KJob*)));
0314     }
0315     return true;
0316 }
0317 
0318 void WebPriceQuote::downloadCSV(KJob* job)
0319 {
0320     QString tmpFile = dynamic_cast<KIO::FileCopyJob*>(job)->destUrl().toLocalFile();
0321     QUrl url = dynamic_cast<KIO::FileCopyJob*>(job)->srcUrl();
0322     if (!job->error())
0323     {
0324         qDebug() << "Downloaded" << tmpFile << "from" << url;
0325         QFile f(tmpFile);
0326         if (f.open(QIODevice::ReadOnly)) {
0327             f.close();
0328             slotParseCSVQuote(tmpFile);
0329         } else {
0330             Q_EMIT error(i18n("Failed to open downloaded file"));
0331             slotParseCSVQuote(QString());
0332         }
0333     } else {
0334         Q_EMIT error(job->errorString());
0335         slotParseCSVQuote(QString());
0336     }
0337 }
0338 
0339 void WebPriceQuote::downloadResult(KJob* job)
0340 {
0341     QString tmpFile = dynamic_cast<KIO::FileCopyJob*>(job)->destUrl().toLocalFile();
0342     QUrl url = dynamic_cast<KIO::FileCopyJob*>(job)->srcUrl();
0343     if (!job->error())
0344     {
0345         qDebug() << "Downloaded" << tmpFile << "from" << url;
0346         QFile f(tmpFile);
0347         if (f.open(QIODevice::ReadOnly)) {
0348             // Find out the page encoding and convert it to unicode
0349             QByteArray page = f.readAll();
0350             KEncodingProber prober(KEncodingProber::Universal);
0351             prober.feed(page);
0352             QTextCodec* codec = QTextCodec::codecForName(prober.encoding());
0353             if (!codec)
0354                 codec = QTextCodec::codecForLocale();
0355             QString quote = codec->toUnicode(page);
0356             f.close();
0357             slotParseQuote(quote, QString());
0358         } else {
0359             Q_EMIT error(i18n("Failed to open downloaded file"));
0360             slotParseQuote(QString(), QString());
0361         }
0362         QFile::remove(tmpFile);
0363     } else {
0364         Q_EMIT error(job->errorString());
0365         slotParseQuote(QString(), QString());
0366     }
0367 }
0368 
0369 bool WebPriceQuote::launchFinanceQuote(const QString& _webID, const QString& _kmmID,
0370                                        const QString& _sourcename)
0371 {
0372     bool result = true;
0373     d->m_webID = _webID;
0374     d->m_kmmID = _kmmID;
0375     QString FQSource = _sourcename.section(' ', 1);
0376     d->m_source = WebPriceQuoteSource(_sourcename, m_financeQuoteScriptPath, m_financeQuoteScriptPath,
0377                                       "\"([^,\"]*)\",.*",  // webIDRegExp
0378                                       WebPriceQuoteSource::identifyBy::Symbol,
0379                                       "[^,]*,[^,]*,\"([^\"]*)\"", // price regexp
0380                                       "[^,]*,([^,]*),.*", // date regexp
0381                                       "%y-%m-%d"); // date format
0382 
0383     //Q_EMIT status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0384 
0385     QStringList arguments;
0386     arguments << m_financeQuoteScriptPath << FQSource << KShell::quoteArg(_webID);
0387     d->m_filter.setWebID(d->m_webID);
0388     Q_EMIT status(i18nc("Executing 'script' 'online source' 'investment symbol' ", "Executing %1 %2 %3...", m_financeQuoteScriptPath, FQSource, _webID));
0389 
0390     // make sure to fix the LD_LIBRARY_PATH
0391     // to not include APPDIR subdirectories
0392     AlkEnvironment::removeAppImagePathFromLinkLoaderLibPath(&d->m_filter);
0393 
0394     d->m_filter.setProcessChannelMode(QProcess::SeparateChannels);
0395     d->m_filter.start(QStringLiteral("perl"), arguments);
0396 
0397     // This seems to work best if we just block until done.
0398     if (d->m_filter.waitForFinished()) {
0399         result = true;
0400     } else {
0401         Q_EMIT error(i18n("Unable to launch: %1", m_financeQuoteScriptPath));
0402         slotParseQuote(QString(), QString());
0403     }
0404 
0405     return result;
0406 }
0407 
0408 void WebPriceQuote::slotParseCSVQuote(const QString& filename)
0409 {
0410     bool isOK = true;
0411     if (filename.isEmpty())
0412         isOK = false;
0413     else {
0414         MyMoneyStatement st;
0415         CSVImporterCore* csvImporter = new CSVImporterCore;
0416         st = csvImporter->unattendedImport(filename, &d->m_CSVSource);
0417         if (!st.m_listPrices.isEmpty())
0418             Q_EMIT csvquote(d->m_kmmID, d->m_webID, st);
0419         else
0420             isOK = false;
0421         delete csvImporter;
0422         QFile::remove(filename);
0423     }
0424 
0425     if (!isOK) {
0426         Q_EMIT error(i18n("Unable to update price for %1", d->m_webID));
0427         Q_EMIT failed(d->m_kmmID, d->m_webID);
0428     }
0429 }
0430 
0431 void WebPriceQuote::slotParseQuote(const QString& _quotedata, const QString& _errordata)
0432 {
0433     QString quotedata = _quotedata;
0434 
0435     qCDebug(WEBPRICEQUOTE) << "quotedata" << _quotedata;
0436     qCDebug(WEBPRICEQUOTE) << "errordata" << _errordata;
0437 
0438     if (_errordata.isEmpty() && !quotedata.isEmpty()) {
0439         if (!d->m_source.m_skipStripping) {
0440             // First, remove extraneous non-data elements
0441 
0442             // HTML tags
0443             quotedata.remove(QRegularExpression("<[^>]*>"));
0444 
0445             // &...;'s
0446             quotedata.replace(QRegularExpression("&\\w+;"), QLatin1String(" "));
0447 
0448             // Extra white space
0449             quotedata = quotedata.simplified();
0450             qCDebug(WEBPRICEQUOTE) << "stripped text" << quotedata;
0451         }
0452 
0453         QRegularExpression webIDRegExp(d->m_source.m_webID);
0454         QRegularExpression dateRegExp(d->m_source.m_date);
0455         QRegularExpression priceRegExp(d->m_source.m_price);
0456         QRegularExpressionMatch match;
0457 
0458         if (quotedata.indexOf(webIDRegExp, 0, &match) > -1) {
0459             qCDebug(WEBPRICEQUOTE) << "Identifier" << match.captured(1);
0460             Q_EMIT status(i18n("Identifier found: '%1'", match.captured(1)));
0461         }
0462 
0463         bool gotprice = false;
0464         bool gotdate = false;
0465 
0466         if (quotedata.indexOf(priceRegExp, 0, &match) > -1) {
0467             gotprice = true;
0468             QString pricestr = match.captured(1);
0469 
0470             // Deal with exponential prices
0471             // we extract the exponent and add it again before we convert to a double
0472             QRegularExpression expRegExp("[eE][+-]?\\D+");
0473             int pos;
0474             QString exponent;
0475             if ((pos = pricestr.indexOf(expRegExp, 0, &match)) > -1) {
0476                 exponent = pricestr.mid(pos);
0477                 pricestr = pricestr.left(pos);
0478             }
0479 
0480             // Deal with european quotes that come back as X.XXX,XX or XX,XXX
0481             //
0482             // We will make the assumption that ALL prices have a decimal separator.
0483             // So "1,000" always means 1.0, not 1000.0.
0484             //
0485             // Remove all non-digits from the price string except the last one, and
0486             // set the last one to a period.
0487 
0488             pos = pricestr.lastIndexOf(QRegularExpression("\\D"));
0489             if (pos > 0) {
0490                 pricestr[pos] = QLatin1Char('.');
0491                 pos = pricestr.lastIndexOf(QRegularExpression("\\D"), pos - 1);
0492             }
0493             while (pos > 0) {
0494                 pricestr.remove(pos, 1);
0495                 pos = pricestr.lastIndexOf(QRegularExpression("\\D"), pos);
0496             }
0497             pricestr.append(exponent);
0498 
0499             d->m_price = pricestr.toDouble();
0500             qCDebug(WEBPRICEQUOTE) << "Price" << pricestr;
0501             Q_EMIT status(i18n("Price found: '%1' (%2)", pricestr, d->m_price));
0502         }
0503 
0504         if (quotedata.indexOf(dateRegExp, 0, &match) > -1) {
0505             QString datestr = match.captured(1);
0506 
0507             MyMoneyDateFormat dateparse(d->m_source.m_dateformat);
0508             try {
0509                 d->m_date = dateparse.convertString(datestr, false /*strict*/);
0510                 gotdate = true;
0511                 qCDebug(WEBPRICEQUOTE) << "Date" << datestr;
0512                 Q_EMIT status(i18n("Date found: '%1'", d->m_date.toString()));
0513             } catch (const MyMoneyException &) {
0514                 // Q_EMIT error(i18n("Unable to parse date %1 using format %2: %3", datestr,dateparse.format(),e.what()));
0515                 d->m_date = QDate::currentDate();
0516                 gotdate = true;
0517             }
0518         }
0519 
0520         if (gotprice && gotdate) {
0521             Q_EMIT quote(d->m_kmmID, d->m_webID, d->m_date, d->m_price);
0522         } else {
0523             Q_EMIT error(i18n("Unable to update price for %1 (no price or no date)", d->m_webID));
0524             Q_EMIT failed(d->m_kmmID, d->m_webID);
0525         }
0526     } else if (_errordata.isEmpty()) {
0527         Q_EMIT error(i18n("Unable to update price for %1 (empty quote data)", d->m_webID));
0528         Q_EMIT failed(d->m_kmmID, d->m_webID);
0529     } else {
0530         Q_EMIT error(i18n("Unable to update price for %1 (error encountered: '%2')", d->m_webID, _errordata));
0531         Q_EMIT failed(d->m_kmmID, d->m_webID);
0532     }
0533 }
0534 
0535 const QMap<QString, PricesProfile> WebPriceQuote::defaultCSVQuoteSources()
0536 {
0537     QMap<QString, PricesProfile> result;
0538 
0539     // tip: possible delimiter indexes are in csvenums.h
0540 
0541     result[QLatin1String("Stooq")] = PricesProfile(QLatin1String("Stooq"),
0542                                      106, 1, 0, DateFormat::YearMonthDay, FieldDelimiter::Semicolon,
0543                                      TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0544     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 4}},
0545     2, Profile::StockPrices);
0546 
0547     result[QLatin1String("Stooq Currency")] = PricesProfile(QLatin1String("Stooq Currency"),
0548             106, 1, 0, DateFormat::YearMonthDay, FieldDelimiter::Semicolon,
0549             TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0550     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 4}},
0551     2, Profile::CurrencyPrices);
0552 
0553     result[QLatin1String("Yahoo")] = PricesProfile(QLatin1String("Yahoo"),
0554                                      106, 1, 0, DateFormat::YearMonthDay, FieldDelimiter::Comma,
0555                                      TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0556     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 4}},
0557     2, Profile::StockPrices);
0558 
0559     result[QLatin1String("Nasdaq Baltic - Shares")] = PricesProfile(QLatin1String("Nasdaq Baltic - Shares"),
0560             106, 1, 0, DateFormat::DayMonthYear, FieldDelimiter::Tab,
0561             TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0562     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 5}},
0563     2, Profile::StockPrices);
0564 
0565     result[QLatin1String("Nasdaq Baltic - Funds")] = PricesProfile(QLatin1String("Nasdaq Baltic - Funds"),
0566             106, 1, 0, DateFormat::DayMonthYear, FieldDelimiter::Tab,
0567             TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0568     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 5}},
0569     2, Profile::StockPrices);
0570     return result;
0571 }
0572 
0573 const QMap<QString, WebPriceQuoteSource> WebPriceQuote::defaultQuoteSources()
0574 {
0575     QMap<QString, WebPriceQuoteSource> result;
0576 
0577     // Use fx-rate.net as the standard currency exchange rate source until
0578     // we have the capability to use more than one source. Use a neutral
0579     // name for the source.
0580     // Update on 2023-08-07: https://discuss.kde.org/t/kmoney-currency-udpates-not-working/3037/3?u=ipwizard
0581     result["KMyMoney Currency"] = WebPriceQuoteSource("KMyMoney Currency",
0582                                                       "https://fx-rate.net/%1/%2",
0583                                                       QString(),
0584                                                       "https://fx-rate.net/([^/]+/[^/]+)",
0585                                                       WebPriceQuoteSource::identifyBy::Symbol,
0586                                                       "Today\\s+=\\s+([^<]+)",
0587                                                       ",\\s*(\\d+\\s*[a-zA-Z]{3}\\s*\\d{4})",
0588                                                       "%d %m %y",
0589                                                       true // skip HTML stripping
0590     );
0591 
0592     // Update on 2017-06 by Łukasz Wojniłowicz
0593     result["Globe & Mail"] = WebPriceQuoteSource("Globe & Mail",
0594                              "http://globefunddb.theglobeandmail.com/gishome/plsql/gis.price_history?pi_fund_id=%1",
0595                              QString(),
0596                              QString(),  // webIDRegExp
0597                              WebPriceQuoteSource::identifyBy::IdentificationNumber,
0598                              "Fund Price:\\D+(\\d+\\.\\d+)", // priceregexp
0599                              "Fund Price:.+as at (\\w+ \\d+, \\d+)\\)", // dateregexp
0600                              "%m %d %y" // dateformat
0601                                                 );
0602 
0603     // Update on 2017-06 by Łukasz Wojniłowicz
0604     result["MSN"] = WebPriceQuoteSource("MSN",
0605                                         "http://www.msn.com/en-us/money/stockdetails/%1",
0606                                         QString(),
0607                                         QString(),  // webIDRegExp
0608                                         WebPriceQuoteSource::identifyBy::Symbol,
0609                                         "(\\d+\\.\\d+) [+-]\\d+.\\d+", // priceregexp
0610                                         "(\\d+/\\d+/\\d+)", //dateregexp
0611                                         "%y %m %d" // dateformat
0612                                        );
0613 
0614     // Finanztreff (replaces VWD.DE) supplied by Michael Zimmerman
0615     result["Finanztreff"] = WebPriceQuoteSource("Finanztreff",
0616                             "http://finanztreff.de/kurse_einzelkurs_detail.htn?u=100&i=%1",
0617                             QString(),
0618                             QString(),  // webIDRegExp
0619                             WebPriceQuoteSource::identifyBy::IdentificationNumber,
0620                             "([0-9]+,\\d+).+Gattung:Fonds", // priceregexp
0621                             "\\).(\\d+\\D+\\d+\\D+\\d+)", // dateregexp (doesn't work; date in chart
0622                             "%d.%m.%y" // dateformat
0623                                                );
0624 
0625     // Finanztreff for fonds supplied by Pierre Metzner
0626     result["FinanztreffFonds"] = WebPriceQuoteSource("Finanztreff - Fonds",
0627                                  "https://fonds.finanztreff.de/fonds_einzelkurs_uebersicht.htn?i=%1",
0628                                  QString(),
0629                                  QString(),  // webIDRegExp
0630                                  WebPriceQuoteSource::identifyBy::IdentificationNumber,
0631                                  "\"fade\">\\D*([0-9\\.]+,\\d+)", // priceregexp
0632                                  "Zeit:.(\\d+\\D+\\d+\\D+\\d+)", // dateregexp
0633                                  "%d.%m.%y", // dateformat
0634                                  true // skip HTML stripping
0635                                                     );
0636 
0637     // Börse Berlin supplied by Pierre Metzner
0638     result["Boerse Berlin"] = WebPriceQuoteSource(QString::fromUtf8("Börse Berlin"),
0639                               "https://www.boerse-berlin.com/index.php/Funds?isin=%1",
0640                               QString(),
0641                               QString(),  // webIDRegExp
0642                               WebPriceQuoteSource::identifyBy::IdentificationNumber,
0643                               "_last\">([0-9,]+\\.\\d+)", // priceregexp
0644                               "_dateTime\">(\\d+\\D+\\d+\\D+\\d+)", // dateregexp
0645                               "%d %m %y", // dateformat
0646                               true // skip HTML stripping
0647                                                  );
0648 
0649     // First revision by Michael Zimmerman
0650     // Update on 2017-06 by Łukasz Wojniłowicz
0651     result["boerseonlineaktien"] = WebPriceQuoteSource("Börse Online - Aktien",
0652                                    "http://www.boerse-online.de/aktie/%1-Aktie",
0653                                    QString(),
0654                                    QString(),  // webIDRegExp
0655                                    WebPriceQuoteSource::identifyBy::Name,
0656                                    "Aktienkurs\\D+(\\d+,\\d{2})", // priceregexp
0657                                    "Datum (\\d{2}\\.\\d{2}\\.\\d{4})", // dateregexp
0658                                    "%d.%m.%y" // dateformat
0659                                                       );
0660 
0661     // This quote source provided by forum post and should replace the previous one:
0662     // Updated on 2023-05-26 by Thomas Baumgart
0663     result["Financial Times - UK Funds"] = WebPriceQuoteSource("Financial Times",
0664                                                                "https://markets.ft.com/data/equities/tearsheet/summary?s=%1",
0665                                                                QString(),
0666                                                                QString(), // webIDRegExp
0667                                                                WebPriceQuoteSource::identifyBy::IdentificationNumber,
0668                                                                "Price\\D+([\\d,]*\\d+\\.\\d+)", // price regexp
0669                                                                "Data delayed at least 15 minutes, as of\\ (.*)\\.", // date regexp
0670                                                                "%m %d %y", // date format
0671                                                                true // skip HTML stripping
0672     );
0673 
0674     // The following two price sources were contributed by
0675     // Marc Zahnlecker <tf2k@users.sourceforge.net>
0676 
0677     result["Wallstreet-Online.DE (Default)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Default)",
0678             "http://www.wallstreet-online.de/si/?k=%1&spid=ws",
0679             QString(),
0680             "Symbol:(\\w+)",  // webIDRegExp
0681             WebPriceQuoteSource::identifyBy::Symbol,
0682             "Letzter Kurs: ([0-9.]+,\\d+)", // priceregexp
0683             ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
0684             "%d %m %y" // dateformat
0685                                                                   );
0686 
0687     // (tf2k) The "mpid" is I think the market place id. In this case five
0688     // stands for Hamburg.
0689     //
0690     // Here the id for several market places: 2 Frankfurt, 3 Berlin, 4
0691     // Düsseldorf, 5 Hamburg, 6 München/Munich, 7 Hannover, 9 Stuttgart, 10
0692     // Xetra, 32 NASDAQ, 36 NYSE
0693 
0694     result["Wallstreet-Online.DE (Hamburg)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Hamburg)",
0695             "http://fonds.wallstreet-online.de/si/?k=%1&spid=ws&mpid=5",
0696             "",
0697             "Symbol:(\\w+)",  // webIDRegExp
0698             WebPriceQuoteSource::identifyBy::Symbol,
0699             "Fonds \\(EUR\\) ([0-9.]+,\\d+)", // priceregexp
0700             ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
0701             "%d %m %y" // dateformat
0702                                                                   );
0703     // First revision on 2017-06 by Łukasz Wojniłowicz
0704     result["Puls Biznesu"] = WebPriceQuoteSource("Puls Biznesu",
0705                              "http://notowania.pb.pl/instrument/%1",
0706                              QString(),
0707                              QString(),                   // webIDRegExp
0708                              WebPriceQuoteSource::identifyBy::IdentificationNumber,
0709                              "(\\d+,\\d{2})\\D+\\d+,\\d{2}%",    // price regexp
0710                              "(\\d{4}-\\d{2}-\\d{2}) \\d{2}:\\d{2}:\\d{2}", // date regexp
0711                              "%y %m %d"                   // date format
0712                                                 );
0713 
0714     // The following price quote was contributed by
0715     // Piotr Adacha <piotr.adacha@googlemail.com>
0716 
0717     // I would like to post new Online Query Settings for KMyMoney. This set is
0718     // suitable to query stooq.com service, providing quotes for stocks, futures,
0719     // mutual funds and other financial instruments from Polish Gielda Papierow
0720     // Wartosciowych (GPW). Unfortunately, none of well-known international
0721     // services provide quotes for this market (biggest one in central and eastern
0722     // Europe), thus, I think it could be helpful for Polish users of KMyMoney (and
0723     // I am one of them for almost a year).
0724 
0725     // Update on 2017-06 by Łukasz Wojniłowicz
0726     result["Stooq"] = WebPriceQuoteSource("Stooq",
0727                                           "http://stooq.com/q/?s=%1",
0728                                           "http://stooq.pl/q/d/l/?s=%1&d1=%y%m%d&d2=%y%m%d&i=d&c=1",
0729                                           QString(),                   // webIDRegExp
0730                                           WebPriceQuoteSource::identifyBy::Symbol,
0731                                           "Last(\\d+\\.\\d+).*Date",    // price regexp
0732                                           "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp
0733                                           "%y %m %d"                   // date format
0734                                          );
0735 
0736     // First revision on 2017-06 by Łukasz Wojniłowicz
0737     result[QLatin1String("Stooq Currency")] = WebPriceQuoteSource("Stooq Currency",
0738             "http://stooq.com/q/?s=%1%2",
0739             "http://stooq.pl/q/d/l/?s=%1%2&d1=%y%m%d&d2=%y%m%d&i=d&c=1",
0740             QString(),                   // webIDRegExp
0741             WebPriceQuoteSource::identifyBy::Symbol,
0742             "Last.*(\\d+\\.\\d+).*Date",    // price regexp
0743             "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp
0744             "%y %m %d"                   // date format
0745                                                                  );
0746 
0747     // First revision on 2017-06 by Łukasz Wojniłowicz
0748     result["Nasdaq Baltic - Shares"] = WebPriceQuoteSource("Nasdaq Baltic - Shares",
0749                                        "http://www.nasdaqbaltic.com/market/?pg=details&instrument=%1&lang=en",
0750                                        "http://www.nasdaqbaltic.com/market/?instrument=%1&pg=details&tab=historical&lang=en&date=&start=%d.%m.%y&end=%d.%m.%y&pg=details&pg2=equity&downloadcsv=1&csv_style=english",
0751                                        QString(),  // webIDRegExp
0752                                        WebPriceQuoteSource::identifyBy::IdentificationNumber,
0753                                        "lastPrice\\D+(\\d+,\\d+)",  // priceregexp
0754                                        "as of: (\\d{2}.\\d{2}.\\d{4})",  // dateregexp
0755                                        "%d.%m.%y",   // dateformat
0756                                        true
0757                                                           );
0758 
0759     // First revision on 2017-06 by Łukasz Wojniłowicz
0760     result["Nasdaq Baltic - Funds"] = WebPriceQuoteSource("Nasdaq Baltic - Funds",
0761                                       "http://www.nasdaqbaltic.com/market/?pg=details&instrument=%1&lang=en",
0762                                       "http://www.nasdaqbaltic.com/market/?instrument=%1&pg=details&tab=historical&lang=en&date=&start=%d.%m.%y&end=%d.%m.%y&pg=details&pg2=equity&downloadcsv=1&csv_style=english",
0763                                       QString(),  // webIDRegExp
0764                                       WebPriceQuoteSource::identifyBy::IdentificationNumber,
0765                                       "marketShareDetailTable(.+\\n){21}\\D+(\\d+)",  // priceregexp
0766                                       "as of: (\\d{2}.\\d{2}.\\d{4})",  // dateregexp
0767                                       "%d.%m.%y",   // dateformat
0768                                       true
0769                                                          );
0770     // The following was contributed by Brendan Coupe <Brendan@CoupeWare.com>
0771     result["Yahoo Finance"] = WebPriceQuoteSource("Yahoo Finance",
0772                               "https://query1.finance.yahoo.com/v8/finance/chart/%1",
0773                               QString(),
0774                               "%1",  // webIDRegExp
0775                               WebPriceQuoteSource::identifyBy::Symbol,
0776                               "\"regularMarketPrice\":((\\d+|\\d{1,3}(?:[,]\\d{3})).\\d+)",  // priceregexp
0777                               "\"regularMarketTime\":([\\d]+)",  // dateregexp
0778                               QString(),
0779                               true
0780                                                  );
0781 
0782     return result;
0783 }
0784 
0785 const QStringList WebPriceQuote::quoteSources(const _quoteSystemE _system)
0786 {
0787     if (_system == Native)
0788         return (quoteSourcesNative());
0789     else
0790         return (quoteSourcesFinanceQuote());
0791 }
0792 
0793 const QStringList WebPriceQuote::quoteSourcesNative()
0794 {
0795     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0796     QStringList groups = kconfig->groupList();
0797 
0798     QStringList::Iterator it;
0799     QRegularExpression onlineQuoteSource(QString("^Online-Quote-Source-(.*)$"));
0800     QRegularExpressionMatch match;
0801 
0802     // get rid of all 'non online quote source' entries
0803     for (it = groups.begin(); it != groups.end(); it = groups.erase(it)) {
0804         if ((*it).indexOf(onlineQuoteSource, 0, &match) >= 0) {
0805             // Insert the name part
0806             it = groups.insert(it, match.captured(1));
0807             ++it;
0808         }
0809     }
0810 
0811     // if the user has the OLD quote source defined, now is the
0812     // time to remove that entry and convert it to the new system.
0813     if (! groups.count() && kconfig->hasGroup("Online Quotes Options")) {
0814         KConfigGroup grp = kconfig->group("Online Quotes Options");
0815         QString url(grp.readEntry("URL", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1"));
0816         QString webIDRegExp(grp.readEntry("SymbolRegex", "\"([^,\"]*)\",.*"));
0817         QString priceRegExp(grp.readEntry("PriceRegex", "[^,]*,([^,]*),.*"));
0818         QString dateRegExp(grp.readEntry("DateRegex", "[^,]*,[^,]*,\"([^\"]*)\""));
0819         kconfig->deleteGroup("Online Quotes Options");
0820 
0821         groups += "Old Source";
0822         grp = kconfig->group(QString(QLatin1String("Online-Quote-Source-%1")).arg("Old Source"));
0823         grp.writeEntry("URL", url);
0824         grp.writeEntry("CSVURL", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1");
0825         grp.writeEntry("IDRegex", webIDRegExp);
0826         grp.writeEntry("PriceRegex", priceRegExp);
0827         grp.writeEntry("DateRegex", dateRegExp);
0828         grp.writeEntry("DateFormatRegex", "%m %d %y");
0829         grp.sync();
0830     }
0831 
0832     // if the user has OLD quote source based only on symbols (and not ISIN)
0833     // now is the time to convert it to the new system.
0834     for (const auto& group : qAsConst(groups)) {
0835         KConfigGroup grp = kconfig->group(QString(QLatin1String("Online-Quote-Source-%1")).arg(group));
0836         if (grp.hasKey("SymbolRegex")) {
0837             grp.writeEntry("IDRegex", grp.readEntry("SymbolRegex"));
0838             grp.deleteEntry("SymbolRegex");
0839         } else
0840             break;
0841     }
0842 
0843     // Set up each of the default sources.  These are done piecemeal so that
0844     // when we add a new source, it's automatically picked up. And any changes
0845     // are also picked up.
0846     QMap<QString, WebPriceQuoteSource> defaults = defaultQuoteSources();
0847     QMap<QString, WebPriceQuoteSource>::const_iterator it_source = defaults.constBegin();
0848     while (it_source != defaults.constEnd()) {
0849         if (! groups.contains((*it_source).m_name)) {
0850             groups += (*it_source).m_name;
0851             (*it_source).write();
0852             kconfig->sync();
0853         }
0854         ++it_source;
0855     }
0856 
0857     return groups;
0858 }
0859 
0860 const QStringList WebPriceQuote::quoteSourcesFinanceQuote()
0861 {
0862     if (m_financeQuoteSources.empty()) { // run the process one time only
0863         // since this is a static function it can be called without constructing an object
0864         // so we need to make sure that m_financeQuoteScriptPath is properly initialized
0865         if (m_financeQuoteScriptPath.isEmpty()) {
0866             m_financeQuoteScriptPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QString("misc/financequote.pl"));
0867         }
0868         FinanceQuoteProcess getList;
0869         getList.launch(m_financeQuoteScriptPath);
0870         while (!getList.isFinished()) {
0871             QCoreApplication::processEvents();
0872         }
0873         m_financeQuoteSources = getList.getSourceList();
0874     }
0875     return (m_financeQuoteSources);
0876 }
0877 
0878 //
0879 // Helper class to load/save an individual source
0880 //
0881 
0882 WebPriceQuoteSource::WebPriceQuoteSource(const QString& name, const QString& url, const QString &csvUrl, const QString& id, const identifyBy idBy, const QString& price, const QString& date, const QString& dateformat, bool skipStripping):
0883     m_name(name),
0884     m_url(url),
0885     m_csvUrl(csvUrl),
0886     m_webID(id),
0887     m_webIDBy(idBy),
0888     m_price(price),
0889     m_date(date),
0890     m_dateformat(dateformat),
0891     m_skipStripping(skipStripping)
0892 { }
0893 
0894 WebPriceQuoteSource::WebPriceQuoteSource(const QString& name)
0895 {
0896     m_name = name;
0897     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0898     KConfigGroup grp = kconfig->group(QString("Online-Quote-Source-%1").arg(m_name));
0899     m_webID = grp.readEntry("IDRegex");
0900     m_webIDBy = static_cast<WebPriceQuoteSource::identifyBy>(grp.readEntry("IDBy", "0").toInt());
0901     m_date = grp.readEntry("DateRegex");
0902     m_dateformat = grp.readEntry("DateFormatRegex", "%m %d %y");
0903     m_price = grp.readEntry("PriceRegex");
0904     m_url = grp.readEntry("URL");
0905     m_csvUrl = grp.readEntry("CSVURL");
0906     m_skipStripping = grp.readEntry("SkipStripping", false);
0907 }
0908 
0909 void WebPriceQuoteSource::write() const
0910 {
0911     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0912     KConfigGroup grp = kconfig->group(QString("Online-Quote-Source-%1").arg(m_name));
0913     grp.writeEntry("URL", m_url);
0914     grp.writeEntry("CSVURL", m_csvUrl);
0915     grp.writeEntry("PriceRegex", m_price);
0916     grp.writeEntry("DateRegex", m_date);
0917     grp.writeEntry("DateFormatRegex", m_dateformat);
0918     grp.writeEntry("IDRegex", m_webID);
0919     grp.writeEntry("IDBy", static_cast<int>(m_webIDBy));
0920     if (m_skipStripping)
0921         grp.writeEntry("SkipStripping", m_skipStripping);
0922     else
0923         grp.deleteEntry("SkipStripping");
0924     kconfig->sync();
0925 }
0926 
0927 void WebPriceQuoteSource::rename(const QString& name)
0928 {
0929     remove();
0930     m_name = name;
0931     write();
0932 }
0933 
0934 void WebPriceQuoteSource::remove() const
0935 {
0936     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0937     kconfig->deleteGroup(QString("Online-Quote-Source-%1").arg(m_name));
0938     kconfig->sync();
0939 }
0940 
0941 //
0942 // Helper class to babysit the KProcess used for running the local script in that case
0943 //
0944 
0945 WebPriceQuoteProcess::WebPriceQuoteProcess()
0946 {
0947     connect(this, &QProcess::readyReadStandardOutput, this, &WebPriceQuoteProcess::slotReceivedDataFromFilter);
0948     connect(this, &QProcess::readyReadStandardError, this, &WebPriceQuoteProcess::slotReceivedErrorFromFilter);
0949     connect(this, &QProcess::finished, this, &WebPriceQuoteProcess::slotProcessExited);
0950 }
0951 
0952 void WebPriceQuoteProcess::slotReceivedDataFromFilter()
0953 {
0954 //   qDebug() << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data);
0955     m_receivedData += QString::fromUtf8(readAllStandardOutput());
0956 }
0957 
0958 void WebPriceQuoteProcess::slotReceivedErrorFromFilter()
0959 {
0960     m_receivedError += QString::fromUtf8(readAllStandardError());
0961 }
0962 
0963 void WebPriceQuoteProcess::slotProcessExited(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/)
0964 {
0965 //   qDebug() << "WebPriceQuoteProcess::slotProcessExited()";
0966     Q_EMIT processExited(m_receivedData, m_receivedError);
0967     m_receivedData.truncate(0);
0968 }
0969 
0970 //
0971 // Helper class to babysit the KProcess used for running the Finance Quote sources script
0972 //
0973 
0974 FinanceQuoteProcess::FinanceQuoteProcess()
0975 {
0976     m_isDone = false;
0977     m_string = "";
0978     m_fqNames["aex"] = "AEX";
0979     m_fqNames["aex_futures"] = "AEX Futures";
0980     m_fqNames["aex_options"] = "AEX Options";
0981     m_fqNames["amfiindia"] = "AMFI India";
0982     m_fqNames["asegr"] = "ASE";
0983     m_fqNames["asia"] = "Asia (Yahoo, ...)";
0984     m_fqNames["asx"] = "ASX";
0985     m_fqNames["australia"] = "Australia (ASX, Yahoo, ...)";
0986     m_fqNames["bmonesbittburns"] = "BMO NesbittBurns";
0987     m_fqNames["brasil"] = "Brasil (Yahoo, ...)";
0988     m_fqNames["canada"] = "Canada (Yahoo, ...)";
0989     m_fqNames["canadamutual"] = "Canada Mutual (Fund Library, ...)";
0990     m_fqNames["deka"] = "Deka Investments";
0991     m_fqNames["dutch"] = "Dutch (AEX, ...)";
0992     m_fqNames["dwsfunds"] = "DWS";
0993     m_fqNames["europe"] = "Europe (Yahoo, ...)";
0994     m_fqNames["fidelity"] = "Fidelity (Fidelity, ...)";
0995     m_fqNames["fidelity_direct"] = "Fidelity Direct";
0996     m_fqNames["financecanada"] = "Finance Canada";
0997     m_fqNames["ftportfolios"] = "First Trust (First Trust, ...)";
0998     m_fqNames["ftportfolios_direct"] = "First Trust Portfolios";
0999     m_fqNames["fundlibrary"] = "Fund Library";
1000     m_fqNames["greece"] = "Greece (ASE, ...)";
1001     m_fqNames["indiamutual"] = "India Mutual (AMFI, ...)";
1002     m_fqNames["maninv"] = "Man Investments";
1003     m_fqNames["fool"] = "Motley Fool";
1004     m_fqNames["nasdaq"] = "Nasdaq (Yahoo, ...)";
1005     m_fqNames["nz"] = "New Zealand (Yahoo, ...)";
1006     m_fqNames["nyse"] = "NYSE (Yahoo, ...)";
1007     m_fqNames["nzx"] = "NZX";
1008     m_fqNames["platinum"] = "Platinum Asset Management";
1009     m_fqNames["seb_funds"] = "SEB";
1010     m_fqNames["sharenet"] = "Sharenet";
1011     m_fqNames["za"] = "South Africa (Sharenet, ...)";
1012     m_fqNames["troweprice_direct"] = "T. Rowe Price";
1013     m_fqNames["troweprice"] = "T. Rowe Price";
1014     m_fqNames["tdefunds"] = "TD Efunds";
1015     m_fqNames["tdwaterhouse"] = "TD Waterhouse Canada";
1016     m_fqNames["tiaacref"] = "TIAA-CREF";
1017     m_fqNames["trustnet"] = "Trustnet";
1018     m_fqNames["uk_unit_trusts"] = "U.K. Unit Trusts";
1019     m_fqNames["unionfunds"] = "Union Investments";
1020     m_fqNames["tsp"] = "US Govt. Thrift Savings Plan";
1021     m_fqNames["usfedbonds"] = "US Treasury Bonds";
1022     m_fqNames["usa"] = "USA (Yahoo, Fool ...)";
1023     m_fqNames["vanguard"] = "Vanguard";
1024     m_fqNames["vwd"] = "VWD";
1025     m_fqNames["yahoo"] = "Yahoo";
1026     m_fqNames["yahoo_asia"] = "Yahoo Asia";
1027     m_fqNames["yahoo_australia"] = "Yahoo Australia";
1028     m_fqNames["yahoo_brasil"] = "Yahoo Brasil";
1029     m_fqNames["yahoo_europe"] = "Yahoo Europe";
1030     m_fqNames["yahoo_nz"] = "Yahoo New Zealand";
1031     m_fqNames["zifunds"] = "Zuerich Investments";
1032     connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter()));
1033     connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcessExited()));
1034     connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(slotProcessExited()));
1035 }
1036 
1037 void FinanceQuoteProcess::slotReceivedDataFromFilter()
1038 {
1039     auto data = QString::fromUtf8(readAllStandardOutput());
1040 
1041 #ifdef Q_OS_WIN
1042     // on Windows we need to change the CR-LF sequence into a simple LF
1043     // Otherwise, the CR will be appended to the name which is then
1044     // confusing the rest of the logic.
1045     data.remove(QLatin1Char('\r'));
1046 #endif
1047 
1048     //   qDebug() << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data);
1049     m_string.append(data);
1050 }
1051 
1052 void FinanceQuoteProcess::slotProcessExited()
1053 {
1054 //   qDebug() << "WebPriceQuoteProcess::slotProcessExited()";
1055     m_isDone = true;
1056 }
1057 
1058 void FinanceQuoteProcess::launch(const QString& scriptPath)
1059 {
1060     QStringList arguments;
1061     arguments << scriptPath << QStringLiteral("-l");
1062     setProcessChannelMode(QProcess::SeparateChannels);
1063 
1064     // make sure to fix the LD_LIBRARY_PATH
1065     // to not include APPDIR subdirectories
1066     AlkEnvironment::removeAppImagePathFromLinkLoaderLibPath(this);
1067 
1068     start(QStringLiteral("perl"), arguments);
1069     if (! waitForStarted()) qWarning("Unable to start FQ script");
1070     return;
1071 }
1072 
1073 const QStringList FinanceQuoteProcess::getSourceList() const
1074 {
1075     QStringList raw = m_string.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1076     QStringList sources;
1077     QStringList::iterator it;
1078     for (it = raw.begin(); it != raw.end(); ++it) {
1079         if (m_fqNames[*it].isEmpty()) sources.append(*it);
1080         else sources.append(m_fqNames[*it]);
1081     }
1082     sources.sort();
1083     return (sources);
1084 }
1085 
1086 const QString FinanceQuoteProcess::crypticName(const QString& niceName) const
1087 {
1088     QString ret(niceName);
1089     fqNameMap::const_iterator it;
1090     for (it = m_fqNames.begin(); it != m_fqNames.end(); ++it) {
1091         if (niceName == it.value()) {
1092             ret = it.key();
1093             break;
1094         }
1095     }
1096     return (ret);
1097 }
1098 
1099 const QString FinanceQuoteProcess::niceName(const QString& crypticName) const
1100 {
1101     QString ret(m_fqNames[crypticName]);
1102     if (ret.isEmpty()) ret = crypticName;
1103     return (ret);
1104 }
1105 //
1106 // Universal date converter
1107 //
1108 
1109 // In 'strict' mode, this is designed to be compatible with the QIF profile date
1110 // converter.  However, that converter deals with the concept of an apostrophe
1111 // format in a way I don't understand.  So for the moment, they are 99%
1112 // compatible, waiting on that issue. (acejones)
1113 
1114 const QDate MyMoneyDateFormat::convertString(const QString& _in, bool _strict, unsigned _centurymidpoint) const
1115 {
1116     //
1117     // Break date format string into component parts
1118     //
1119 
1120     QRegularExpression formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", QRegularExpression::CaseInsensitiveOption);
1121     QRegularExpressionMatch match;
1122     if (m_format.indexOf(formatrex, 0, &match) == -1) {
1123         throw MYMONEYEXCEPTION_CSTRING("Invalid format string");
1124     }
1125 
1126     QStringList formatParts;
1127     formatParts += match.captured(1);
1128     formatParts += match.captured(3);
1129     formatParts += match.captured(5);
1130 
1131     QStringList formatDelimiters;
1132     formatDelimiters += match.captured(2);
1133     formatDelimiters += match.captured(4);
1134     match = QRegularExpressionMatch();
1135 
1136     //
1137     // Break input string up into component parts,
1138     // using the delimiters found in the format string
1139     //
1140 
1141     QRegularExpression inputrex;
1142     inputrex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
1143 
1144     // strict mode means we must enforce the delimiters as specified in the
1145     // format.  non-strict allows any delimiters
1146     if (_strict)
1147         inputrex.setPattern(QString("(\\w+)%1(\\w+)%2(\\w+)").arg(formatDelimiters[0], formatDelimiters[1]));
1148     else
1149         inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
1150 
1151     if (_in.indexOf(inputrex, 0, &match) == -1) {
1152         throw MYMONEYEXCEPTION_CSTRING("Invalid input string");
1153     }
1154 
1155     QStringList scannedParts;
1156     scannedParts += match.captured(1).toLower();
1157     scannedParts += match.captured(2).toLower();
1158     scannedParts += match.captured(3).toLower();
1159     match = QRegularExpressionMatch();
1160 
1161     //
1162     // Convert the scanned parts into actual date components
1163     //
1164     unsigned day = 0, month = 0, year = 0;
1165     bool ok;
1166     QRegularExpression digitrex("(\\d+)");
1167     QStringList::const_iterator it_scanned = scannedParts.constBegin();
1168     QStringList::const_iterator it_format = formatParts.constBegin();
1169     while (it_scanned != scannedParts.constEnd()) {
1170         // decide upon the first character of the part
1171         switch ((*it_format).at(0).cell()) {
1172         case 'd':
1173             // remove any extraneous non-digits (e.g. read "3rd" as 3)
1174             ok = false;
1175             if ((*it_scanned).indexOf(digitrex, 0, &match) != -1)
1176                 day = match.captured(1).toUInt(&ok);
1177             if (!ok || day > 31)
1178                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid day entry: %1").arg(*it_scanned));
1179             break;
1180         case 'm':
1181             month = (*it_scanned).toUInt(&ok);
1182             if (!ok) {
1183                 month = 0;
1184                 // maybe it's a textual date
1185                 unsigned i = 1;
1186                 // search the name in the current selected locale
1187                 QLocale locale;
1188                 while (i <= 12) {
1189                     if (locale.standaloneMonthName(i).toLower() == *it_scanned
1190                             || locale.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
1191                         month = i;
1192                         break;
1193                     }
1194                     ++i;
1195                 }
1196                 // in case we did not find the month in the current locale,
1197                 // we look for it in the C locale
1198                 if(month == 0) {
1199                     QLocale localeC(QLocale::C);
1200                     if( !(locale == localeC)) {
1201                         i = 1;
1202                         while (i <= 12) {
1203                             if (localeC.standaloneMonthName(i).toLower() == *it_scanned
1204                                     || localeC.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
1205                                 month = i;
1206                                 break;
1207                             }
1208                             ++i;
1209                         }
1210                     }
1211                 }
1212             }
1213 
1214             if (month < 1 || month > 12)
1215                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid month entry: %1").arg(*it_scanned));
1216 
1217             break;
1218         case 'y':
1219             if (_strict && (*it_scanned).length() != (*it_format).length())
1220                 throw MYMONEYEXCEPTION(QString::fromLatin1("Length of year (%1) does not match expected length (%2).")
1221                                        .arg(*it_scanned, *it_format));
1222 
1223             year = (*it_scanned).toUInt(&ok);
1224 
1225             if (!ok)
1226                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid year entry: %1").arg(*it_scanned));
1227 
1228             //
1229             // 2-digit year case
1230             //
1231             // this algorithm will pick a year within +/- 50 years of the
1232             // centurymidpoint parameter.  i.e. if the midpoint is 2000,
1233             // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
1234             if (year < 100) {
1235                 unsigned centuryend = _centurymidpoint + 50;
1236                 unsigned centurybegin = _centurymidpoint - 50;
1237 
1238                 if (year < centuryend % 100)
1239                     year += 100;
1240                 year += centurybegin - centurybegin % 100;
1241             }
1242 
1243             if (year < 1900)
1244                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid year (%1)").arg(year));
1245 
1246             break;
1247         default:
1248             throw MYMONEYEXCEPTION_CSTRING("Invalid format character");
1249         }
1250 
1251         ++it_scanned;
1252         ++it_format;
1253     }
1254     QDate result(year, month, day);
1255     if (! result.isValid())
1256         throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid date (yr%1 mo%2 dy%3)").arg(year).arg(month).arg(day));
1257 
1258     return result;
1259 }
1260 
1261 //
1262 // Unit test helpers
1263 //
1264 
1265 convertertest::QuoteReceiver::QuoteReceiver(WebPriceQuote* q, QObject* parent) :
1266     QObject(parent)
1267 {
1268     connect(q, SIGNAL(quote(QString,QString,QDate,double)),
1269             this, SLOT(slotGetQuote(QString,QString,QDate,double)));
1270     connect(q, SIGNAL(status(QString)),
1271             this, SLOT(slotStatus(QString)));
1272     connect(q, SIGNAL(error(QString)),
1273             this, SLOT(slotError(QString)));
1274 }
1275 
1276 convertertest::QuoteReceiver::~QuoteReceiver()
1277 {
1278 }
1279 
1280 void convertertest::QuoteReceiver::slotGetQuote(const QString&, const QString&, const QDate& d, const double& m)
1281 {
1282 //   qDebug() << "test::QuoteReceiver::slotGetQuote( , " << d << " , " << m.toString() << " )";
1283 
1284     m_price = MyMoneyMoney(m);
1285     m_date = d;
1286 }
1287 
1288 void convertertest::QuoteReceiver::slotStatus(const QString& msg)
1289 {
1290 //   qDebug() << "test::QuoteReceiver::slotStatus( " << msg << " )";
1291 
1292     m_statuses += msg;
1293 }
1294 
1295 void convertertest::QuoteReceiver::slotError(const QString& msg)
1296 {
1297 //   qDebug() << "test::QuoteReceiver::slotError( " << msg << " )";
1298 
1299     m_errors += msg;
1300 }
1301 
1302 // leave this moc until we will have resolved our dependency issues
1303 // now 'converter' depends on 'kmymoney' a pointer to the application
1304 // defined in main.cpp, which makes this static library unusable without
1305 // the --as-needed linker flag;
1306 // otherwise the 'moc' methods of this object will be linked into the automoc
1307 // object file which contains methods from all the other objects from this
1308 // library, thus even if the --as-needed option is used all objects will be
1309 // pulled in while linking 'convertertest' which only needs the WebPriceQuote
1310 // object - spent a whole day investigating this
1311 #include "moc_webpricequote.cpp"