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"