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