gnucash/libgnucash/app-utils/gnc-quotes.cpp
Maarten Bosmans 6be682b645 Store allocated temporaries in a variable so they can be freed
If a function that returns an allocated pointer is passed directly into
something that does not take ownership of the pointer, the allocation is
leaked.  This can be fixed by assigning the pointer to a new variable
and freeing it after operation on the memory.
2023-04-29 11:51:43 -07:00

1084 lines
34 KiB
C++

/********************************************************************\
* gnc-quotes.hpp -- proxy for Finance::Quote *
* Copyright (C) 2021 Geert Janssens <geert@kobaltwit.be> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\ *******************************************************************/
#include <config.h>
#include <algorithm>
#include <stdexcept>
#include <vector>
#include <string>
#include <iostream>
#include <boost/version.hpp>
#if BOOST_VERSION < 107600
// json_parser uses a deprecated version of bind.hpp
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#endif
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/process.hpp>
#include <boost/regex.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream_buffer.hpp>
#include <boost/locale.hpp>
#include <boost/asio.hpp>
#include <glib.h>
#include "gnc-commodity.hpp"
#include <gnc-datetime.hpp>
#include <gnc-numeric.hpp>
#include "gnc-quotes.hpp"
#include <gnc-commodity.h>
#include <gnc-path.h>
#include "gnc-ui-util.h"
#include <gnc-prefs.h>
#include <gnc-session.h>
#include <regex.h>
#include <qofbook.h>
static const QofLogModule log_module = "gnc.price-quotes";
namespace bl = boost::locale;
namespace bp = boost::process;
namespace bfs = boost::filesystem;
namespace bpt = boost::property_tree;
namespace bio = boost::iostreams;
using QuoteResult = std::tuple<int, StrVec, StrVec>;
struct GncQuoteSourceError : public std::runtime_error
{
GncQuoteSourceError(const std::string& err) : std::runtime_error(err) {}
};
CommVec
gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
class GncQuoteSource
{
public:
virtual ~GncQuoteSource() = default;
virtual const StrVec& get_sources() const noexcept = 0;
virtual const std::string & get_version() const noexcept = 0;
virtual QuoteResult get_quotes(const std::string& json_str) const = 0;
};
class GncQuotesImpl
{
public:
// Constructor - checks for presence of Finance::Quote and import version and quote sources
GncQuotesImpl ();
explicit GncQuotesImpl (QofBook *book);
GncQuotesImpl(QofBook*, std::unique_ptr<GncQuoteSource>);
void fetch (QofBook *book);
void fetch (CommVec& commodities);
void fetch (gnc_commodity *comm);
void report (const char* source, const StrVec& commodities, bool verbose);
const std::string& version() noexcept { return m_quotesource->get_version(); }
const QuoteSources& sources() noexcept { return m_sources; }
GList* sources_as_glist ();
bool had_failures() noexcept { return !m_failures.empty(); }
const QFVec& failures() noexcept;
std::string report_failures() noexcept;
private:
std::string query_fq (const char* source, const StrVec& commoditites);
std::string query_fq (const CommVec&);
bpt::ptree parse_quotes (const std::string& quote_str);
void create_quotes(const bpt::ptree& pt, const CommVec& comm_vec);
std::string comm_vec_to_json_string(const CommVec&) const;
GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*);
std::unique_ptr<GncQuoteSource> m_quotesource;
QuoteSources m_sources;
QFVec m_failures;
QofBook *m_book;
gnc_commodity *m_dflt_curr;
};
class GncFQQuoteSource final : public GncQuoteSource
{
const bfs::path c_cmd;
std::string c_fq_wrapper;
std::string m_version;
StrVec m_sources;
std::string m_api_key;
public:
GncFQQuoteSource();
~GncFQQuoteSource() = default;
const std::string& get_version() const noexcept override { return m_version; }
const StrVec& get_sources() const noexcept override { return m_sources; }
QuoteResult get_quotes(const std::string&) const override;
private:
QuoteResult run_cmd (const StrVec& args, const std::string& json_string) const;
};
static void show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
static void show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
static std::string parse_quotesource_error(const std::string& line);
static const std::string empty_string{};
GncFQQuoteSource::GncFQQuoteSource() :
c_cmd{bp::search_path("perl")},
m_version{}, m_sources{}, m_api_key{}
{
char *bindir = gnc_path_get_bindir();
c_fq_wrapper = std::string(bindir) + "/finance-quote-wrapper";
g_free(bindir);
StrVec args{"-w", c_fq_wrapper, "-v"};
auto [rv, sources, errors] = run_cmd(args, empty_string);
if (rv)
{
std::string err{bl::translate("Failed to initialize Finance::Quote: ")};
for (const auto& err_line : errors)
err += err_line.empty() ? "" : err_line + "\n";
throw(GncQuoteSourceError(err));
}
if (!errors.empty())
{
std::string err{bl::translate("Finance::Quote check returned error ")};
for(const auto& err_line : errors)
err += err.empty() ? "" : err_line + "\n";
throw(GncQuoteSourceError(err));
}
auto version{sources.front()};
if (version.empty())
{
std::string err{bl::translate("No Finance::Quote Version")};
throw(GncQuoteSourceError(err));
}
m_version = std::move(version);
sources.erase(sources.begin());
m_sources = std::move(sources);
std::sort (m_sources.begin(), m_sources.end());
auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
if (!(av_key && *av_key))
{
g_free (av_key);
av_key = g_strdup(getenv("ALPHAVANTAGE_API_KEY"));
}
if (av_key)
{
m_api_key = std::string(av_key);
g_free (av_key);
}
else
PWARN("No Alpha Vantage API key set, currency quotes and other AlphaVantage based quotes won't work.");
}
QuoteResult
GncFQQuoteSource::get_quotes(const std::string& json_str) const
{
StrVec args{"-w", c_fq_wrapper, "-f" };
return run_cmd(args, json_str);
}
QuoteResult
GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) const
{
StrVec out_vec, err_vec;
int cmd_result;
try
{
std::future<std::vector<char> > out_buf, err_buf;
boost::asio::io_service svc;
auto input_buf = bp::buffer (json_string);
bp::child process;
if (m_api_key.empty())
process = bp::child(c_cmd, args,
bp::std_out > out_buf,
bp::std_err > err_buf,
bp::std_in < input_buf,
svc);
else
process = bp::child(c_cmd, args,
bp::std_out > out_buf,
bp::std_err > err_buf,
bp::std_in < input_buf,
bp::env["ALPHAVANTAGE_API_KEY"] = m_api_key,
svc);
svc.run();
process.wait();
{
auto raw = out_buf.get();
std::vector<std::string> data;
std::string line;
bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
std::istream is(&sb);
while (std::getline(is, line) && !line.empty())
{
#ifdef __WIN32
if (line.back() == '\r')
line.pop_back();
#endif
out_vec.push_back (std::move(line));
}
raw = err_buf.get();
bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
std::istream es(&eb);
while (std::getline(es, line) && !line.empty())
err_vec.push_back (std::move(line));
}
cmd_result = process.exit_code();
}
catch (std::exception &e)
{
cmd_result = -1;
err_vec.push_back(e.what());
};
return QuoteResult (cmd_result, std::move(out_vec), std::move(err_vec));
}
/* GncQuotes implementation */
GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
m_sources{}, m_failures{},
m_book{qof_session_get_book(gnc_get_current_session())},
m_dflt_curr{gnc_default_currency()}
{
m_sources = m_quotesource->get_sources();
}
GncQuotesImpl::GncQuotesImpl(QofBook* book) : m_quotesource{new GncFQQuoteSource},
m_sources{}, m_book{book},
m_dflt_curr{gnc_default_currency()}
{
m_sources = m_quotesource->get_sources();
}
GncQuotesImpl::GncQuotesImpl(QofBook* book, std::unique_ptr<GncQuoteSource> quote_source) :
m_quotesource{std::move(quote_source)},
m_sources{}, m_book{book}, m_dflt_curr{gnc_default_currency()}
{
m_sources = m_quotesource->get_sources();
}
GList*
GncQuotesImpl::sources_as_glist()
{
GList* slist = nullptr;
std::for_each (m_sources.rbegin(), m_sources.rend(),
[&slist](const std::string& source) { slist = g_list_prepend (slist, g_strdup(source.c_str())); });
return slist;
}
void
GncQuotesImpl::fetch (QofBook *book)
{
if (!book)
throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no book.")));
auto commodities = gnc_quotes_get_quotable_commodities (
gnc_commodity_table_get_table (book));
fetch (commodities);
}
void
GncQuotesImpl::fetch (gnc_commodity *comm)
{
auto commodities = CommVec {comm};
fetch (commodities);
}
void
GncQuotesImpl::fetch (CommVec& commodities)
{
m_failures.clear();
if (commodities.empty())
throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no commodities.")));
auto quote_str{query_fq (commodities)};
auto ptree{parse_quotes (quote_str)};
create_quotes(ptree, commodities);
}
void
GncQuotesImpl::report (const char* source, const StrVec& commodities,
bool verbose)
{
bool is_currency{source && strcmp(source, "currency") == 0};
m_failures.clear();
if (commodities.empty())
{
std::cerr << _("There were no commodities for which to retrieve quotes.") << std::endl;
return;
}
try
{
auto quote_str{query_fq (source, commodities)};
auto ptree{parse_quotes (quote_str)};
if (is_currency)
show_currency_quotes(ptree, commodities, verbose);
else
show_quotes(ptree, commodities, verbose);
}
catch (const GncQuoteException& err)
{
std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl;
}
}
const QFVec&
GncQuotesImpl::failures() noexcept
{
return m_failures;
}
static std::string
explain(GncQuoteError err, const std::string& errmsg)
{
std::string retval;
switch (err)
{
case GncQuoteError::NO_RESULT:
if (errmsg.empty())
retval += _("Finance::Quote returned no data and set no error.");
else
retval += _("Finance::Quote returned an error: ") + errmsg;
break;
case GncQuoteError::QUOTE_FAILED:
if (errmsg.empty())
retval += _("Finance::Quote reported failure set no error.");
else
retval += _("Finance::Quote reported failure with error: ") + errmsg;
break;
case GncQuoteError::NO_CURRENCY:
retval += _("Finance::Quote returned a quote with no currency.");
break;
case GncQuoteError::UNKNOWN_CURRENCY:
retval += _("Finance::Quote returned a quote with a currency GnuCash doesn't know about.");
break;
case GncQuoteError::NO_PRICE:
retval += _("Finance::Quote returned a quote with no price element.");
break;
case GncQuoteError::PRICE_PARSE_FAILURE:
retval += _("Finance::Quote returned a quote with a price that GnuCash was unable to covert to a number.");
break;
case GncQuoteError::SUCCESS:
default:
retval += _("The quote has no error set.");
break;
}
return retval;
}
std::string
GncQuotesImpl::report_failures() noexcept
{
std::string retval{_("Quotes for the following commodities were unavailable or unusable:\n")};
std::for_each(m_failures.begin(), m_failures.end(),
[&retval](auto failure)
{
auto [ns, sym, reason, err] = failure;
retval += "* " + ns + ":" + sym + " " +
explain(reason, err) + "\n";
});
return retval;
}
/* **** Private function implementations ****/
std::string
GncQuotesImpl::comm_vec_to_json_string (const CommVec& comm_vec) const
{
bpt::ptree pt, pt_child;
pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
std::for_each (comm_vec.cbegin(), comm_vec.cend(),
[this, &pt] (auto comm)
{
auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
auto comm_ns = std::string("currency");
if (gnc_commodity_is_currency (comm))
{
if (gnc_commodity_equiv(comm, m_dflt_curr) ||
(!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
return;
}
else
comm_ns = gnc_quote_source_get_internal_name (gnc_commodity_get_quote_source (comm));
auto key = comm_ns + "." + comm_mnemonic;
pt.put (key, "");
}
);
std::ostringstream result;
bpt::write_json(result, pt);
return result.str();
}
static inline std::string
get_quotes(const std::string& json_str, const std::unique_ptr<GncQuoteSource>& qs)
{
auto [rv, quotes, errors] = qs->get_quotes(json_str);
std::string answer;
if (rv == 0)
{
for (const auto& line : quotes)
answer.append(line + "\n");
}
else
{
std::string err_str;
for (const auto& line: errors)
{
if (line == "invalid_json\n")
PERR("Finanace Quote Wrapper was unable to parse %s",
json_str.c_str());
err_str += parse_quotesource_error(line);
}
throw(GncQuoteException(err_str));
}
return answer;
}
std::string
GncQuotesImpl::query_fq (const char* source, const StrVec& commodities)
{
bpt::ptree pt;
auto is_currency{strcmp(source, "currency") == 0};
if (is_currency && commodities.size() < 2)
throw(GncQuoteException(_("Currency quotes requires at least two currencies")));
if (is_currency)
pt.put("defaultcurrency", commodities[0].c_str());
else
pt.put("defaultcurrency", gnc_commodity_get_mnemonic(m_dflt_curr));
std::for_each(is_currency ? ++commodities.cbegin() : commodities.cbegin(),
commodities.cend(),
[source, &pt](auto sym)
{
using Path = bpt::ptree::path_type;
Path key{source, '|'};
key /= Path{sym, '|'};
pt.put(key, "");
});
std::ostringstream result;
bpt::write_json(result, pt);
return get_quotes(result.str(), m_quotesource);
}
std::string
GncQuotesImpl::query_fq (const CommVec& comm_vec)
{
auto json_str{comm_vec_to_json_string(comm_vec)};
return get_quotes(json_str, m_quotesource);
}
struct PriceParams
{
const char* ns;
const char* mnemonic;
bool success;
std::string type;
boost::optional<std::string> price;
bool inverted;
boost::optional<std::string> date;
boost::optional<std::string> time;
boost::optional<std::string> currency;
boost::optional<std::string> errormsg;
};
static void
get_price_and_type(PriceParams& p, const bpt::ptree& comm_pt)
{
p.type = "last";
p.price = comm_pt.get_optional<std::string> (p.type);
if (!p.price)
{
p.type = "nav";
p.price = comm_pt.get_optional<std::string> (p.type);
}
if (!p.price)
{
p.type = "price";
p.price = comm_pt.get_optional<std::string> (p.type);
/* guile wrapper used "unknown" as price type when "price" was found,
* reproducing here to keep same result for users in the pricedb */
p.type = p.price ? "unknown" : "missing";
}
}
static void
parse_quote_json(PriceParams& p, const bpt::ptree& comm_pt)
{
auto success = comm_pt.get_optional<bool> ("success");
p.success = success && *success;
if (!p.success)
p.errormsg = comm_pt.get_optional<std::string> ("errormsg");
get_price_and_type(p, comm_pt);
auto inverted = comm_pt.get_optional<bool> ("inverted");
p.inverted = inverted && *inverted;
p.date = comm_pt.get_optional<std::string> ("date");
p.time = comm_pt.get_optional<std::string> ("time");
p.currency = comm_pt.get_optional<std::string> ("currency");
PINFO("Commodity: %s", p.mnemonic);
PINFO(" Success: %s", (inverted ? "yes" : "no"));
PINFO(" Date: %s", (p.date ? p.date->c_str() : "missing"));
PINFO(" Time: %s", (p.time ? p.time->c_str() : "missing"));
PINFO(" Currency: %s", (p.currency ? p.currency->c_str() : "missing"));
PINFO(" Price: %s", (p.price ? p.price->c_str() : "missing"));
PINFO(" Inverted: %s\n", (inverted ? "yes" : "no"));
}
static time64
calc_price_time(const PriceParams& p)
{
/* Note that as of F::Q v. 1.52 the only sources that provide
* quote times are ftfunds (aka ukfunds), morningstarch, and
* mstaruk_fund, but it's faked with a comment "Set a dummy time
* as gnucash insists on having a valid format". It's also wrong,
* as it lacks seconds. Best ignored.
*/
if (p.date)
{
/* Returned date is always in MM/DD/YYYY format according to
* F::Q man page, transform it to simplify conversion to
* GncDateTime.
*/
auto date_tmp = *p.date;
auto iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) +
"-" + date_tmp.substr (3, 2);
try
{
auto close_time{GncDateTime(iso_date_str + " 16:00:00")};
PINFO("Quote date included, using %s for %s:%s",
close_time.format("%Y-%m-%d %H:%M:%S").c_str(), p.ns, p.mnemonic);
return static_cast<time64>(close_time);
}
catch (...)
{
auto now{GncDateTime()};
PWARN("Warning: failed to parse quote date '%s' for %s:%s - will use %s",
iso_date_str.c_str(), p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M%S").c_str());
return static_cast<time64>(now);
}
}
auto now{GncDateTime()};
PINFO("No date was returned for %s:%s - will use %s",
p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M%S").c_str());
return static_cast<time64>(now);
}
static boost::optional<GncNumeric>
get_price(const PriceParams& p)
{
boost::optional<GncNumeric> price;
try
{
price = GncNumeric { *p.price };
}
catch (...)
{
PWARN("Skipped %s:%s - failed to parse returned price '%s'",
p.ns, p.mnemonic, p.price->c_str());
}
if (price && p.inverted)
*price = price->inv();
return price;
}
static gnc_commodity*
get_currency(const PriceParams& p, QofBook* book, QFVec& failures)
{
if (!p.currency)
{
failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_CURRENCY,
empty_string);
PWARN("Skipped %s:%s - Finance::Quote returned a quote with no currency",
p.ns, p.mnemonic);
return nullptr;
}
std::string curr_str = *p.currency;
boost::to_upper (curr_str);
auto commodity_table = gnc_commodity_table_get_table (book);
auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", curr_str.c_str());
if (!currency)
{
failures.emplace_back(p.ns, p.mnemonic,
GncQuoteError::UNKNOWN_CURRENCY, empty_string);
PWARN("Skipped %s:%s - failed to parse returned currency '%s'",
p.ns, p.mnemonic, p.currency->c_str());
return nullptr;
}
return currency;
}
GNCPrice*
GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
{
PriceParams p;
p.ns = gnc_commodity_get_namespace (comm);
p.mnemonic = gnc_commodity_get_mnemonic (comm);
if (gnc_commodity_equiv(comm, m_dflt_curr) ||
(!p.mnemonic || (strcmp (p.mnemonic, "XXX") == 0)))
return nullptr;
auto comm_pt_ai{pt.find(p.mnemonic)};
if (comm_pt_ai == pt.not_found())
{
m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_RESULT,
empty_string);
PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
p.ns, p.mnemonic);
return nullptr;
}
auto comm_pt{comm_pt_ai->second};
parse_quote_json(p, comm_pt);
if (!p.success)
{
m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::QUOTE_FAILED,
p.errormsg ? *p.errormsg : empty_string);
PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
p.ns, p.mnemonic,
(p.errormsg ? p.errormsg->c_str() : "unknown"));
return nullptr;
}
if (!p.price)
{
m_failures.emplace_back(p.ns, p.mnemonic,
GncQuoteError::NO_PRICE, empty_string);
PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
p.ns, p.mnemonic);
return nullptr;
}
auto price{get_price(p)};
if (!price)
{
m_failures.emplace_back(p.ns, p.mnemonic,
GncQuoteError::PRICE_PARSE_FAILURE,
empty_string);
return nullptr;
}
auto currency{get_currency(p, m_book, m_failures)};
if (!currency)
return nullptr;
auto quotedt{calc_price_time(p)};
auto gnc_price = gnc_price_create (m_book);
gnc_price_begin_edit (gnc_price);
gnc_price_set_commodity (gnc_price, comm);
gnc_price_set_currency (gnc_price, currency);
gnc_price_set_time64 (gnc_price, static_cast<time64> (quotedt));
gnc_price_set_source (gnc_price, PRICE_SOURCE_FQ);
gnc_price_set_typestr (gnc_price, p.type.c_str());
gnc_price_set_value (gnc_price, *price);
gnc_price_commit_edit (gnc_price);
return gnc_price;
}
bpt::ptree
GncQuotesImpl::parse_quotes (const std::string& quote_str)
{
bpt::ptree pt;
std::istringstream ss {quote_str};
const char* what = nullptr;
try
{
bpt::read_json (ss, pt);
}
catch (bpt::json_parser_error &e) {
what = e.what();
}
catch (const std::runtime_error& e)
{
what = e.what();
}
catch (const std::logic_error& e)
{
what = e.what();
}
catch (...) {
std::string error_msg{_("Failed to parse result returned by Finance::Quote.")};
throw(GncQuoteException(error_msg));
}
if (what)
{
std::string error_msg{_("Failed to parse result returned by Finance::Quote.")};
error_msg += "\n";
error_msg += _("Error message:");
error_msg += "\n";
error_msg += what;
throw(GncQuoteException(error_msg));
}
return pt;
}
void
GncQuotesImpl::create_quotes (const bpt::ptree& pt, const CommVec& comm_vec)
{
auto pricedb{gnc_pricedb_get_db(m_book)};
for (auto comm : comm_vec)
{
auto price{parse_one_quote(pt, comm)};
if (!price)
continue;
gnc_price_begin_edit (price);
gnc_pricedb_add_price(pricedb, price);
gnc_price_commit_edit(price);
gnc_price_unref (price);
}
}
static void
show_verbose_quote(const bpt::ptree& comm_pt)
{
std::for_each(comm_pt.begin(), comm_pt.end(),
[](auto elem) {
std::cout << std::setw(12) << std::right << elem.first << " => " <<
std::left << elem.second.data() << "\n";
});
std::cout << std::endl;
}
static void
show_gnucash_quote(const bpt::ptree& comm_pt)
{
constexpr const char* ptr{"<=== "};
constexpr const char* dptr{"<=\\ "};
constexpr const char* uptr{"<=/ "};
//Translators: Means that the preceding element is required
const char* reqd{C_("Finance::Quote", "required")};
//Translators: Means that the quote will work best if the preceding element is provided
const char* rec{C_("Finance::Quote", "recommended")};
//Translators: Means that one of the indicated elements is required
const char* oot{C_("Finance::Quote", "one of these")};
//Translators: Means that a required element wasn't reported. The *s are for emphasis.
const char* miss{C_("Finance::Quote", "**missing**")};
const std::string miss_str{miss};
auto outline{[](const char* label, std::string value, const char* pointer, const char* req) {
std::cout << std::setw(12) << std::right << label << std::setw(16) << std::left <<
value << pointer << req << "\n";
}};
std::cout << _("Finance::Quote fields GnuCash uses:") << "\n";
//Translators: The stock or Mutual Fund symbol, ISIN, CUSIP, etc.
outline(C_("Finance::Quote", "symbol: "), comm_pt.get<char>("symbol", miss), ptr, reqd);
//Translators: The date of the quote.
outline(C_("Finance::Quote", "date: "), comm_pt.get<char>("date", miss), ptr, rec);
//Translators: The quote currency
outline(C_("Finance::Quote", "currency: "), comm_pt.get<char>("currency", miss), ptr, reqd);
auto last{comm_pt.get<char>("last", "")};
auto nav{comm_pt.get<char>("nav", "")};
auto price{comm_pt.get<char>("nav", "")};
auto no_price{last.empty() && nav.empty() && price.empty()};
//Translators: The quote is for the most recent trade on the exchange
outline(C_("Finance::Quote", "last: "), no_price ? miss_str : last, dptr, "");
//Translators: The quote is for an open-ended mutual fund and represents the net asset value of one unit of the fund at the previous close of trading.
outline(C_("Finance::Quote", "nav: "), no_price ? miss_str : nav, ptr, oot);
//Translators: The quote is neither a last trade nor an NAV.
outline(C_("Finance::Quote", "price: "), no_price ? miss_str : price, uptr, "");
std::cout << std::endl;
}
static const bpt::ptree empty_tree{};
static inline const bpt::ptree&
get_commodity_data(const bpt::ptree& pt, const std::string& comm)
{
auto commdata{pt.find(comm)};
if (commdata == pt.not_found())
{
std::cout << comm << " " << _("Finance::Quote returned no data and set no error.") << std::endl;
return empty_tree;
}
auto& comm_pt{commdata->second};
auto success = comm_pt.get_optional<bool> ("success");
if (!(success && *success))
{
auto errormsg = comm_pt.get_optional<std::string> ("errormsg");
if (errormsg && !errormsg->empty())
std::cout << _("Finance::Quote reported a failure for symbol ") <<
comm << ": " << *errormsg << std::endl;
else
std::cout << _("Finance::Quote failed silently to retrieve a quote for symbol ") <<
comm << std::endl;
return empty_tree;
}
return comm_pt;
}
static void
show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose)
{
for (const auto& comm : commodities)
{
auto comm_pt{get_commodity_data(pt, comm)};
if (comm_pt == empty_tree)
continue;
if (verbose)
{
std::cout << comm << ":\n";
show_verbose_quote(comm_pt);
}
else
{
show_gnucash_quote(comm_pt);
}
}
}
static void
show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose)
{
auto to_cur{commodities.front()};
for (const auto& comm : commodities)
{
if (comm == to_cur)
continue;
auto comm_pt{get_commodity_data(pt, comm)};
if (comm_pt == empty_tree)
continue;
if (verbose)
{
std::cout << comm << ":\n";
show_verbose_quote(comm_pt);
}
else
{
std::cout << "1 " << comm << " = " <<
comm_pt.get<char>("last", "Not Found") << " " << to_cur << "\n";
}
std::cout << std::endl;
}
}
static std::string
parse_quotesource_error(const std::string& line)
{
std::string err_str;
if (line == "invalid_json\n")
{
err_str += _("GnuCash submitted invalid json to Finance::Quote. The details were logged.");
}
else if (line.substr(0, 15) == "missing_modules")
{
PERR("Missing Finance::Quote Dependencies: %s",
line.substr(17).c_str());
err_str += _("Perl is missing the following modules. Please see https://wiki.gnucash.org/wiki/Online_Quotes#Finance::Quote for detailed corrective action. ");
err_str += line.substr(17);
}
else
{
PERR("Unrecognized Finance::Quote Error %s", line.c_str());
err_str +=_("Unrecognized Finance::Quote Error: ");
err_str += line;
}
err_str += "\n";
return err_str;
}
/********************************************************************
* gnc_quotes_get_quotable_commodities
* list commodities in a given namespace that get price quotes
********************************************************************/
/* Helper function to be passed to g_list_for_each applied to the result
* of gnc_commodity_namespace_get_commodity_list.
*/
static void
get_quotables_helper1 (gpointer value, gpointer data)
{
auto l = static_cast<CommVec *> (data);
auto comm = static_cast<gnc_commodity *> (value);
auto quote_flag = gnc_commodity_get_quote_flag (comm);
auto quote_source = gnc_commodity_get_quote_source (comm);
auto quote_source_supported = gnc_quote_source_get_supported (quote_source);
if (!quote_flag ||
!quote_source || !quote_source_supported)
return;
l->push_back (comm);
}
// Helper function to be passed to gnc_commodity_table_for_each
static gboolean
get_quotables_helper2 (gnc_commodity *comm, gpointer data)
{
auto l = static_cast<CommVec *> (data);
auto quote_flag = gnc_commodity_get_quote_flag (comm);
auto quote_source = gnc_commodity_get_quote_source (comm);
auto quote_source_supported = gnc_quote_source_get_supported (quote_source);
if (!quote_flag ||
!quote_source || !quote_source_supported)
return TRUE;
l->push_back (comm);
return TRUE;
}
CommVec
gnc_quotes_get_quotable_commodities (const gnc_commodity_table * table)
{
gnc_commodity_namespace * ns = NULL;
const char *name_space;
GList * nslist, * tmp;
CommVec l;
regex_t pattern;
const char *expression = gnc_prefs_get_namespace_regexp ();
// ENTER("table=%p, expression=%s", table, expression);
if (!table)
return CommVec ();
if (expression && *expression)
{
if (regcomp (&pattern, expression, REG_EXTENDED | REG_ICASE) != 0)
{
// LEAVE ("Cannot compile regex");
return CommVec ();
}
nslist = gnc_commodity_table_get_namespaces (table);
for (tmp = nslist; tmp; tmp = tmp->next)
{
name_space = static_cast<const char *> (tmp->data);
if (regexec (&pattern, name_space, 0, NULL, 0) == 0)
{
// DEBUG ("Running list of %s commodities", name_space);
ns = gnc_commodity_table_find_namespace (table, name_space);
if (ns)
{
auto cm_list = gnc_commodity_namespace_get_commodity_list (ns);
g_list_foreach (cm_list, &get_quotables_helper1, (gpointer) &l);
}
}
}
g_list_free (nslist);
regfree (&pattern);
}
else
{
gnc_commodity_table_foreach_commodity (table, get_quotables_helper2,
(gpointer) &l);
}
//LEAVE ("list head %p", &l);
return l;
}
/* Public interface functions */
// Constructor - checks for presence of Finance::Quote and import version and quote sources
GncQuotes::GncQuotes ()
{
try
{
m_impl = std::make_unique<GncQuotesImpl>();
}
catch (const GncQuoteSourceError& err)
{
throw(GncQuoteException(err.what()));
}
}
void
GncQuotes::fetch (QofBook *book)
{
m_impl->fetch (book);
}
void GncQuotes::fetch (CommVec& commodities)
{
m_impl->fetch (commodities);
}
void GncQuotes::fetch (gnc_commodity *comm)
{
m_impl->fetch (comm);
}
void GncQuotes::report (const char* source, const StrVec& commodities,
bool verbose)
{
m_impl->report(source, commodities, verbose);
}
const std::string& GncQuotes::version() noexcept
{
return m_impl->version ();
}
const QuoteSources& GncQuotes::sources() noexcept
{
return m_impl->sources ();
}
GList* GncQuotes::sources_as_glist ()
{
return m_impl->sources_as_glist ();
}
GncQuotes::~GncQuotes() = default;
bool
GncQuotes::had_failures() noexcept
{
return m_impl->had_failures();
}
const QFVec&
GncQuotes::failures() noexcept
{
return m_impl->failures();
}
const std::string
GncQuotes::report_failures() noexcept
{
return m_impl->report_failures();
}