diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 588c77fe..fe2b7994 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -33,6 +33,6 @@ jobs: working-directory: ${{github.workspace}}/build run: cmake --build . --config ${{matrix.buildmode}} --parallel 4 - - name: Local Tests + - name: Tests working-directory: ${{github.workspace}}/build run: ctest -j 4 -C ${{matrix.buildmode}} --output-on-failure diff --git a/src/api/common/include/exchangepublicapi.hpp b/src/api/common/include/exchangepublicapi.hpp index 8e75dcf6..03c63351 100644 --- a/src/api/common/include/exchangepublicapi.hpp +++ b/src/api/common/include/exchangepublicapi.hpp @@ -4,7 +4,6 @@ #include #include -#include "cct_string.hpp" #include "commonapi.hpp" #include "currencycode.hpp" #include "currencyexchangeflatset.hpp" @@ -184,14 +183,13 @@ class ExchangePublic : public ExchangeBase { ExchangePublic(std::string_view name, FiatConverter &fiatConverter, CommonAPI &commonApi, const CoincenterInfo &coincenterInfo); - string _name; + std::string_view _name; CachedResultVault _cachedResultVault; FiatConverter &_fiatConverter; CommonAPI &_commonApi; const CoincenterInfo &_coincenterInfo; const ExchangeConfig &_exchangeConfig; - std::mutex _tradableMarketsMutex; - std::mutex _allOrderBooksMutex; + std::recursive_mutex _publicRequestsMutex; }; } // namespace api } // namespace cct diff --git a/src/api/common/src/exchangepublicapi.cpp b/src/api/common/src/exchangepublicapi.cpp index 39c25326..4a345373 100644 --- a/src/api/common/src/exchangepublicapi.cpp +++ b/src/api/common/src/exchangepublicapi.cpp @@ -54,7 +54,7 @@ std::optional ExchangePublic::convert(MonetaryAmount from, Curre priceOptions.isTakerStrategy() ? ExchangeConfig::FeeType::kTaker : ExchangeConfig::FeeType::kMaker; if (marketOrderBookMap.empty()) { - std::lock_guard guard(_allOrderBooksMutex); + std::lock_guard guard(_publicRequestsMutex); marketOrderBookMap = queryAllApproximatedOrderBooks(1); } @@ -210,7 +210,7 @@ MarketsPath ExchangePublic::findMarketsPath(CurrencyCode fromCurrency, CurrencyC // Retrieve markets if not already done if (markets.empty()) { - std::lock_guard guard(_tradableMarketsMutex); + std::lock_guard guard(_publicRequestsMutex); markets = queryTradableMarkets(); if (markets.empty()) { log::error("No markets retrieved for {}", _name); @@ -307,7 +307,7 @@ std::optional ExchangePublic::RetrieveMarket(CurrencyCode c1, CurrencyCo } std::optional ExchangePublic::retrieveMarket(CurrencyCode c1, CurrencyCode c2) { - std::lock_guard guard(_tradableMarketsMutex); + std::lock_guard guard(_publicRequestsMutex); return RetrieveMarket(c1, c2, queryTradableMarkets()); } @@ -355,7 +355,7 @@ std::optional ExchangePublic::determineMarketFromMarketStr(std::string_v if (markets.empty()) { // Without any currency, and because "marketStr" is returned without hyphen, there is no easy way to guess the // currencies so we need to compare with the markets that exist - std::lock_guard guard(_tradableMarketsMutex); + std::lock_guard guard(_publicRequestsMutex); markets = queryTradableMarkets(); } const auto symbolStrSize = marketStr.size(); @@ -382,7 +382,7 @@ std::optional ExchangePublic::determineMarketFromMarketStr(std::string_v Market ExchangePublic::determineMarketFromFilterCurrencies(MarketSet &markets, CurrencyCode filterCur1, CurrencyCode filterCur2) { if (markets.empty()) { - std::lock_guard guard(_tradableMarketsMutex); + std::lock_guard guard(_publicRequestsMutex); markets = queryTradableMarkets(); } diff --git a/src/api/exchanges/include/upbitpublicapi.hpp b/src/api/exchanges/include/upbitpublicapi.hpp index 6b581e25..551caf16 100644 --- a/src/api/exchanges/include/upbitpublicapi.hpp +++ b/src/api/exchanges/include/upbitpublicapi.hpp @@ -4,6 +4,7 @@ #include #include "cachedresult.hpp" +#include "cct_string.hpp" #include "curlhandle.hpp" #include "currencycodeset.hpp" #include "exchangepublicapi.hpp" @@ -83,7 +84,7 @@ class UpbitPublic : public ExchangePublic { struct WithdrawalFeesFunc { MonetaryAmountByCurrencySet operator()(); - const string& _name; + std::string_view _name; std::string_view _dataDir; }; diff --git a/src/api/interface/include/exchange.hpp b/src/api/interface/include/exchange.hpp index cfb18540..a542430f 100644 --- a/src/api/interface/include/exchange.hpp +++ b/src/api/interface/include/exchange.hpp @@ -37,6 +37,8 @@ class Exchange { std::string_view name() const { return apiPublic().name(); } std::string_view keyName() const { return apiPrivate().keyName(); } + std::size_t publicExchangePos() const; + ExchangeName createExchangeName() const { if (hasPrivateAPI()) { return {name(), keyName()}; diff --git a/src/api/interface/src/exchange.cpp b/src/api/interface/src/exchange.cpp index cb86645b..dd50a361 100644 --- a/src/api/interface/src/exchange.cpp +++ b/src/api/interface/src/exchange.cpp @@ -20,6 +20,8 @@ Exchange::Exchange(const ExchangeConfig &exchangeConfig, ExchangePublic &exchang _exchangePrivate(std::move(exchangePrivate)), _pExchangeConfig(std::addressof(exchangeConfig)) {} +std::size_t Exchange::publicExchangePos() const { return PublicExchangePos(name()); } + bool Exchange::canWithdraw(CurrencyCode currencyCode, const CurrencyExchangeFlatSet ¤cyExchangeSet) const { if (_pExchangeConfig->excludedCurrenciesWithdrawal().contains(currencyCode)) { return false; diff --git a/src/engine/include/coincentercommand.hpp b/src/engine/include/coincentercommand.hpp index 6764b547..0f91f2d8 100644 --- a/src/engine/include/coincentercommand.hpp +++ b/src/engine/include/coincentercommand.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -61,7 +62,7 @@ class CoincenterCommand { MonetaryAmount amount() const { return _amount; } int depth() const { return _n; } - std::optional optDepth() const { return _n == -1 ? std::nullopt : std::optional(_n); } + auto optDepth() const { return _n == -1 ? std::nullopt : std::optional(_n); } Market market() const { return _market; } @@ -87,7 +88,7 @@ class CoincenterCommand { MonetaryAmount _amount; Market _market; CurrencyCode _cur1, _cur2; - int _n = -1; + int32_t _n = -1; CoincenterCommandType _type; bool _isPercentageAmount = false; bool _withBalanceInUse = false; diff --git a/src/engine/include/commandlineoptionsparser.hpp b/src/engine/include/commandlineoptionsparser.hpp index 99e722f8..b25dcb3c 100644 --- a/src/engine/include/commandlineoptionsparser.hpp +++ b/src/engine/include/commandlineoptionsparser.hpp @@ -49,35 +49,28 @@ class CommandLineOptionsParser { CommandLineOptionsParser& append(std::ranges::input_range auto&& opts) { const auto insertedIt = _opts.insert(_opts.end(), std::ranges::begin(opts), std::ranges::end(opts)); const auto sortByFirst = [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }; + std::sort(insertedIt, _opts.end(), sortByFirst); std::inplace_merge(_opts.begin(), insertedIt, _opts.end(), sortByFirst); + return *this; } - OptValueType parse(std::span groupedArguments) { - _callbacks.clear(); - _callbacks.reserve(_opts.size()); + auto parse(std::span groupedArguments) { OptValueType data; - for (const auto& [cmdLineOption, prop] : _opts) { - _callbacks[cmdLineOption] = registerCallback(cmdLineOption, prop, data); - } + + registerCallbacks(data); + const int nbArgs = static_cast(groupedArguments.size()); for (int argPos = 0; argPos < nbArgs; ++argPos) { std::string_view argStr(groupedArguments[argPos]); - if (std::ranges::none_of(_opts, [argStr](const auto& opt) { return opt.first.matches(argStr); })) { - const auto [possibleOptionIdx, minDistance] = minLevenshteinDistanceOpt(argStr); - auto existingOptionStr = _opts[possibleOptionIdx].first.fullName(); - if (minDistance <= 2 || - minDistance < static_cast(std::min(argStr.size(), existingOptionStr.size()) / 2)) { - throw invalid_argument("Unrecognized command-line option '{}' - did you mean '{}'?", argStr, - existingOptionStr); - } - throw invalid_argument("Unrecognized command-line option '{}'", argStr); + if (std::ranges::none_of(_opts, [argStr](const auto& opt) { return opt.first.matches(argStr); })) { + invalidArgument(argStr); } - for (auto& callback : _callbacks) { - callback.second(argPos, groupedArguments); + for (auto& [_, callback] : _callbacks) { + callback(argPos, groupedArguments); } } @@ -138,7 +131,8 @@ class CommandLineOptionsParser { private: static constexpr std::string_view kEmptyLine = - " " + " " + " " " "; static constexpr int kMaxCharLine = kEmptyLine.length(); @@ -147,7 +141,7 @@ class CommandLineOptionsParser { template friend class CommandLineOptionsParserIterator; - using CallbackType = std::function)>; + using CallbackType = std::function)>; [[nodiscard]] bool isOptionValue(std::string_view opt) const { return std::ranges::none_of(_opts, [opt](const auto& cmdLineOpt) { return cmdLineOpt.first.matches(opt); }); @@ -162,71 +156,71 @@ class CommandLineOptionsParser { CallbackType registerCallback(const CommandLineOption& commandLineOption, CommandLineOptionType prop, OptValueType& data) { - return [this, &commandLineOption, prop, &data](int& idx, std::span argv) { - if (commandLineOption.matches(argv[idx])) { - std::visit(overloaded{ - // integral value matcher including bool - [&data, &idx, argv, &commandLineOption](std::integral auto OptValueType::*arg) { - using IntType = std::remove_reference_t; - if constexpr (std::is_same_v) { - data.*arg = true; - } else { - if (idx + 1U < argv.size()) { - std::string_view nextOpt(argv[idx + 1]); - if (IsOptionInt(nextOpt)) { - data.*arg = FromString(nextOpt); - ++idx; - return; - } - } - ThrowExpectingValueException(commandLineOption); - } - }, + return [this, &commandLineOption, prop, &data](int& idx, std::span argv) { + if (!commandLineOption.matches(argv[idx])) { + return; + } - // CommandLineOptionalInt value matcher - [&data, &idx, argv](CommandLineOptionalInt OptValueType::*arg) { - data.*arg = CommandLineOptionalInt(CommandLineOptionalInt::State::kOptionPresent); + std::visit(overloaded{ + // integral value matcher including bool + [&data, &idx, argv, &commandLineOption](std::integral auto OptValueType::*arg) { + using IntType = std::remove_reference_t; + if constexpr (std::is_same_v) { + data.*arg = true; + } else { if (idx + 1U < argv.size()) { - std::string_view nextOpt(argv[idx + 1]); - if (IsOptionInt(nextOpt)) { - data.*arg = FromString(nextOpt); + std::string_view opt(argv[idx + 1]); + if (IsOptionInt(opt)) { + data.*arg = FromString(opt); ++idx; + return; } } - }, - - // std::string_view value matcher - [&data, &idx, argv, &commandLineOption](std::string_view OptValueType::*arg) { - if (idx + 1U < argv.size()) { - data.*arg = std::string_view(argv[idx + 1]); - ++idx; - } else { - ThrowExpectingValueException(commandLineOption); - } - }, - - // optional std::string_view value matcher - [this, &data, &idx, argv](std::optional OptValueType::*arg) { - if (idx + 1U < argv.size() && this->isOptionValue(argv[idx + 1])) { - data.*arg = std::string_view(argv[idx + 1]); + ThrowExpectingValueException(commandLineOption); + } + }, + + // CommandLineOptionalInt value matcher + [&data, &idx, argv](CommandLineOptionalInt OptValueType::*arg) { + data.*arg = CommandLineOptionalInt(CommandLineOptionalInt::State::kOptionPresent); + if (idx + 1U < argv.size()) { + std::string_view opt(argv[idx + 1]); + if (IsOptionInt(opt)) { + data.*arg = FromString(opt); ++idx; - } else { - data.*arg = std::string_view(); } - }, - - // duration value matcher - [&data, &idx, argv, &commandLineOption](Duration OptValueType::*arg) { - if (idx + 1U < argv.size()) { - data.*arg = ParseDuration(argv[idx + 1]); - ++idx; - } else { - ThrowExpectingValueException(commandLineOption); - } - }, - }, - prop); - } + } + }, + + // std::string_view value matcher + [&data, &idx, argv, &commandLineOption](std::string_view OptValueType::*arg) { + if (idx + 1U < argv.size()) { + data.*arg = std::string_view(argv[++idx]); + return; + } + ThrowExpectingValueException(commandLineOption); + }, + + // optional std::string_view value matcher + [this, &data, &idx, argv](std::optional OptValueType::*arg) { + if (idx + 1U < argv.size() && this->isOptionValue(argv[idx + 1])) { + data.*arg = std::string_view(argv[idx + 1]); + ++idx; + return; + } + data.*arg = std::string_view(); + }, + + // duration value matcher + [&data, &idx, argv, &commandLineOption](Duration OptValueType::*arg) { + if (idx + 1U < argv.size()) { + data.*arg = ParseDuration(argv[++idx]); + return; + } + ThrowExpectingValueException(commandLineOption); + }, + }, + prop); }; } @@ -262,6 +256,17 @@ class CommandLineOptionsParser { return lenFirstRows + 3; } + void invalidArgument(std::string_view argStr) const { + const auto [possibleOptionIdx, minDistance] = minLevenshteinDistanceOpt(argStr); + auto existingOptionStr = _opts[possibleOptionIdx].first.fullName(); + + if (minDistance <= 2 || + minDistance < static_cast(std::min(argStr.size(), existingOptionStr.size()) / 2)) { + throw invalid_argument("Unrecognized command-line option '{}' - did you mean '{}'?", argStr, existingOptionStr); + } + throw invalid_argument("Unrecognized command-line option '{}'", argStr); + } + std::pair minLevenshteinDistanceOpt(std::string_view argStr) const { vector minDistancesToFullNameOptions(_opts.size()); LevenshteinDistanceCalculator calc; @@ -271,6 +276,14 @@ class CommandLineOptionsParser { return {optIt - minDistancesToFullNameOptions.begin(), *optIt}; } + void registerCallbacks(OptValueType& data) { + _callbacks.reserve(_opts.size()); + _callbacks.clear(); + for (const auto& [cmdLineOption, prop] : _opts) { + _callbacks[cmdLineOption] = registerCallback(cmdLineOption, prop, data); + } + } + vector _opts; std::unordered_map _callbacks; }; diff --git a/src/engine/include/commandlineoptionsparseriterator.hpp b/src/engine/include/commandlineoptionsparseriterator.hpp index 8c87392d..f313836a 100644 --- a/src/engine/include/commandlineoptionsparseriterator.hpp +++ b/src/engine/include/commandlineoptionsparseriterator.hpp @@ -11,7 +11,7 @@ template class CommandLineOptionsParserIterator { public: CommandLineOptionsParserIterator(const CommandLineOptionsParser& parser, - std::span allArguments) + std::span allArguments) : _parser(parser), _allArguments(allArguments), _begIt(_allArguments.begin()), @@ -26,8 +26,8 @@ class CommandLineOptionsParserIterator { * @brief Get next grouped arguments that should be treated together. * hasNext needs to return true prior to the call to this method */ - std::span next() { - std::span ret(_begIt, _endIt); + auto next() { + std::span ret(_begIt, _endIt); _begIt = _endIt; _endIt = getNextGroupedEndIt(_endIt); _hasReturnedAtLeastOneSpan = true; @@ -35,7 +35,7 @@ class CommandLineOptionsParserIterator { } private: - using ConstIt = std::span::iterator; + using ConstIt = std::span::iterator; [[nodiscard]] ConstIt getNextGroupedEndIt(ConstIt searchFromIt) const { if (searchFromIt == _allArguments.end()) { @@ -46,6 +46,7 @@ class CommandLineOptionsParserIterator { for (const auto& [cmdLineOption, _] : _parser._opts) { std::string_view cmdLineOptionFullName = cmdLineOption.fullName(); if (cmdLineOptionFullName[0] != '-' && cmdLineOptionFullName == optStr) { + // It's a new command name return searchFromIt; } } @@ -55,7 +56,7 @@ class CommandLineOptionsParserIterator { } const CommandLineOptionsParser& _parser; - std::span _allArguments; + std::span _allArguments; ConstIt _begIt; ConstIt _endIt; bool _hasReturnedAtLeastOneSpan = false; diff --git a/src/engine/include/metricsexporter.hpp b/src/engine/include/metricsexporter.hpp index d3679ab8..c9374cd4 100644 --- a/src/engine/include/metricsexporter.hpp +++ b/src/engine/include/metricsexporter.hpp @@ -1,7 +1,6 @@ #pragma once #include "currencycode.hpp" -#include "market.hpp" #include "queryresulttypes.hpp" namespace cct { @@ -18,9 +17,9 @@ class MetricsExporter { void exportTickerMetrics(const ExchangeTickerMaps &marketOrderBookMaps); - void exportOrderbookMetrics(Market mk, const MarketOrderBookConversionRates &marketOrderBookConversionRates); + void exportOrderbookMetrics(const MarketOrderBookConversionRates &marketOrderBookConversionRates); - void exportLastTradesMetrics(Market mk, const TradesPerExchange &lastTradesPerExchange); + void exportLastTradesMetrics(const TradesPerExchange &lastTradesPerExchange); private: void createSummariesAndHistograms(); diff --git a/src/engine/include/parseoptions.hpp b/src/engine/include/parseoptions.hpp index f34ada49..5cf158e4 100644 --- a/src/engine/include/parseoptions.hpp +++ b/src/engine/include/parseoptions.hpp @@ -10,11 +10,12 @@ #include "commandlineoptionsparseriterator.hpp" namespace cct { + template auto ParseOptions(ParserType &parser, int argc, const char *argv[]) { auto programName = std::filesystem::path(argv[0]).filename().string(); - std::span allArguments(argv, argc); + std::span allArguments(argv, argc); // skip first argument which is program name CommandLineOptionsParserIterator parserIt(parser, allArguments.last(allArguments.size() - 1U)); @@ -29,6 +30,7 @@ auto ParseOptions(ParserType &parser, int argc, const char *argv[]) { auto groupedArguments = parserIt.next(); auto groupParsedOptions = parser.parse(groupedArguments); + globalOptions.mergeGlobalWith(groupParsedOptions); if (groupedArguments.empty()) { @@ -36,12 +38,17 @@ auto ParseOptions(ParserType &parser, int argc, const char *argv[]) { } if (groupParsedOptions.help) { parser.displayHelp(programName, std::cout); - } else if (groupParsedOptions.version) { + parsedOptions.clear(); + break; + } + if (groupParsedOptions.version) { CoincenterCmdLineOptions::PrintVersion(programName, std::cout); - } else { - // Only store commands if they are not 'help' nor 'version' - parsedOptions.push_back(std::move(groupParsedOptions)); + parsedOptions.clear(); + break; } + + // Only store commands if they are not 'help' nor 'version' + parsedOptions.push_back(std::move(groupParsedOptions)); } // Apply global options to all parsed options containing commands diff --git a/src/engine/include/stringoptionparser.hpp b/src/engine/include/stringoptionparser.hpp index eda347e2..58bed922 100644 --- a/src/engine/include/stringoptionparser.hpp +++ b/src/engine/include/stringoptionparser.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -26,11 +25,15 @@ class StringOptionParser { /// If FieldIs is kOptional and there is no currency, default currency code will be returned. /// otherwise exception invalid_argument will be raised - CurrencyCode parseCurrency(FieldIs fieldIs = FieldIs::kMandatory); + /// @param delimiter defines the expected character (could be not present, which means end of parsing) + /// after the currency + CurrencyCode parseCurrency(FieldIs fieldIs = FieldIs::kMandatory, char delimiter = ','); /// If FieldIs is kOptional and there is no market, default market will be returned. - /// otherwise exception invalid_argument will be raised - Market parseMarket(FieldIs fieldIs = FieldIs::kMandatory); + /// otherwise exception invalid_argument will be raised. + /// @param delimiter defines the expected character (could be not present, which means end of parsing) + /// after the market + Market parseMarket(FieldIs fieldIs = FieldIs::kMandatory, char delimiter = ','); /// If FieldIs is kOptional and there is no amount, AmountType kNotPresent will be returned /// otherwise exception invalid_argument will be raised @@ -50,6 +53,6 @@ class StringOptionParser { private: std::string_view _opt; - std::size_t _pos{}; + std::string_view::size_type _pos{}; }; } // namespace cct \ No newline at end of file diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index ac40c511..a7d69459 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -15,6 +15,7 @@ #include "coincenterinfo.hpp" #include "currencycode.hpp" #include "depositsconstraints.hpp" +#include "durationstring.hpp" #include "exchange-names.hpp" #include "exchangename.hpp" #include "exchangepublicapi.hpp" @@ -72,18 +73,29 @@ Coincenter::Coincenter(const CoincenterInfo &coincenterInfo, const ExchangeSecre } int Coincenter::process(const CoincenterCommands &coincenterCommands) { - int nbCommandsProcessed = 0; const auto commands = coincenterCommands.commands(); const int nbRepeats = commands.empty() ? 0 : coincenterCommands.repeats(); + const auto repeatTime = coincenterCommands.repeatTime(); + + int nbCommandsProcessed = 0; + TimePoint lastCommandTime; for (int repeatPos = 0; repeatPos != nbRepeats && g_signalStatus == 0; ++repeatPos) { - if (repeatPos != 0) { - std::this_thread::sleep_for(coincenterCommands.repeatTime()); + const auto earliestTimeNextCommand = lastCommandTime + repeatTime; + lastCommandTime = Clock::now(); + + if (earliestTimeNextCommand > lastCommandTime) { + const auto waitingDuration = earliestTimeNextCommand - lastCommandTime; + + lastCommandTime += waitingDuration; + + log::debug("Sleep for {} before next command", DurationToString(waitingDuration)); + std::this_thread::sleep_for(waitingDuration); } if (nbRepeats != 1) { if (nbRepeats == -1) { - log::info("Processing request {}", repeatPos + 1); + log::info("Process request {}", repeatPos + 1); } else { - log::info("Processing request {}/{}", repeatPos + 1, nbRepeats); + log::info("Process request {}/{}", repeatPos + 1, nbRepeats); } } TransferableCommandResultVector transferableResults; @@ -274,7 +286,7 @@ MarketOrderBookConversionRates Coincenter::getMarketOrderBooks(Market mk, Exchan std::optional depth) { const auto ret = _exchangesOrchestrator.getMarketOrderBooks(mk, exchangeNames, equiCurrencyCode, depth); - _metricsExporter.exportOrderbookMetrics(mk, ret); + _metricsExporter.exportOrderbookMetrics(ret); return ret; } @@ -396,7 +408,7 @@ MonetaryAmountPerExchange Coincenter::getLast24hTradedVolumePerExchange(Market m TradesPerExchange Coincenter::getLastTradesPerExchange(Market mk, ExchangeNameSpan exchangeNames, int nbLastTrades) { const auto ret = _exchangesOrchestrator.getLastTradesPerExchange(mk, exchangeNames, nbLastTrades); - _metricsExporter.exportLastTradesMetrics(mk, ret); + _metricsExporter.exportLastTradesMetrics(ret); return ret; } @@ -407,8 +419,10 @@ MonetaryAmountPerExchange Coincenter::getLastPricePerExchange(Market mk, Exchang void Coincenter::updateFileCaches() const { log::debug("Store all cache files"); + _commonAPI.updateCacheFile(); _fiatConverter.updateCacheFile(); + std::ranges::for_each(_exchangePool.exchanges(), [](const Exchange &exchange) { exchange.updateCacheFile(); }); } diff --git a/src/engine/src/metricsexporter.cpp b/src/engine/src/metricsexporter.cpp index 90d56b41..a73ce772 100644 --- a/src/engine/src/metricsexporter.cpp +++ b/src/engine/src/metricsexporter.cpp @@ -90,13 +90,12 @@ void MetricsExporter::exportTickerMetrics(const ExchangeTickerMaps &marketOrderB } } -void MetricsExporter::exportOrderbookMetrics(Market mk, - const MarketOrderBookConversionRates &marketOrderBookConversionRates) { +void MetricsExporter::exportOrderbookMetrics(const MarketOrderBookConversionRates &marketOrderBookConversionRates) { RETURN_IF_NO_MONITORING; MetricKey key = CreateMetricKey("limit_pri", "Best bids and asks prices"); - string marketLowerCase = mk.assetsPairStrLower('-'); - key.append("market", marketLowerCase); + for (const auto &[exchangeName, marketOrderBook, optConversionRate] : marketOrderBookConversionRates) { + key.set("market", marketOrderBook.market().assetsPairStrLower('-')); key.set("exchange", exchangeName); key.set("side", "ask"); _pMetricsGateway->add(MetricType::kGauge, MetricOperation::kSet, key, marketOrderBook.lowestAskPrice().toDouble()); @@ -106,6 +105,7 @@ void MetricsExporter::exportOrderbookMetrics(Market mk, key.set(kMetricNameKey, "limit_vol"); key.set(kMetricHelpKey, "Best bids and asks volumes"); for (const auto &[exchangeName, marketOrderBook, optConversionRate] : marketOrderBookConversionRates) { + key.set("market", marketOrderBook.market().assetsPairStrLower('-')); key.set("exchange", exchangeName); key.set("side", "ask"); _pMetricsGateway->add(MetricType::kGauge, MetricOperation::kSet, key, @@ -116,13 +116,16 @@ void MetricsExporter::exportOrderbookMetrics(Market mk, } } -void MetricsExporter::exportLastTradesMetrics(Market mk, const TradesPerExchange &lastTradesPerExchange) { +void MetricsExporter::exportLastTradesMetrics(const TradesPerExchange &lastTradesPerExchange) { RETURN_IF_NO_MONITORING; MetricKey key = CreateMetricKey("", "All public trades that occurred on the market"); - string marketLowerCase = mk.assetsPairStrLower('-'); - key.append("market", marketLowerCase); for (const auto &[e, lastTrades] : lastTradesPerExchange) { + if (lastTrades.empty()) { + continue; + } + Market mk = lastTrades.front().market(); + key.set("market", mk.assetsPairStrLower('-')); key.set("exchange", e->name()); std::array totalAmounts{MonetaryAmount(0, mk.base()), MonetaryAmount(0, mk.base())}; diff --git a/src/engine/src/stringoptionparser.cpp b/src/engine/src/stringoptionparser.cpp index 9eb7813b..11502459 100644 --- a/src/engine/src/stringoptionparser.cpp +++ b/src/engine/src/stringoptionparser.cpp @@ -15,6 +15,47 @@ #include "monetaryamount.hpp" namespace cct { + +// At the end of the currency, either the end of the string or a comma is expected. +CurrencyCode StringOptionParser::parseCurrency(FieldIs fieldIs, char delimiter) { + const auto delimiterPos = _opt.find(delimiter, _pos); + const auto begIt = _opt.begin() + _pos; + const bool isDelimiterPresent = delimiterPos != std::string_view::npos; + const std::string_view tokenStr(begIt, isDelimiterPresent ? _opt.begin() + delimiterPos : _opt.end()); + + if (!tokenStr.empty() && !ExchangeName::IsValid(tokenStr) && CurrencyCode::IsValid(tokenStr)) { + // disambiguate currency code from exchange name + _pos += tokenStr.size(); + if (isDelimiterPresent) { + ++_pos; + } + return tokenStr; + } + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a valid currency code in '{}'", std::string_view(_opt.begin() + _pos, _opt.end())); + } + + return {}; +} + +// At the end of the market, either the end of the string or a comma is expected. +Market StringOptionParser::parseMarket(FieldIs fieldIs, char delimiter) { + const auto oldPos = _pos; + + CurrencyCode firstCur = parseCurrency(fieldIs, '-'); + CurrencyCode secondCur; + + if (firstCur.isDefined()) { + secondCur = parseCurrency(fieldIs, delimiter); + if (secondCur.isNeutral()) { + firstCur = CurrencyCode(); + _pos = oldPos; + } + } + + return {firstCur, secondCur}; +} + namespace { template @@ -39,70 +80,12 @@ std::string_view GetNextStr(std::string_view opt, CharOrStringType sep, std::siz } // namespace -// At the end of the currency, either the end of the string or a comma is expected. -CurrencyCode StringOptionParser::parseCurrency(FieldIs fieldIs) { - const std::size_t commaPos = _opt.find(',', _pos); - const auto begIt = _opt.begin() + _pos; - const bool isCommaPresent = commaPos != std::string_view::npos; - const std::string_view firstStr(begIt, isCommaPresent ? _opt.begin() + commaPos : _opt.end()); - - std::string_view curStr; - if (!firstStr.empty() && !ExchangeName::IsValid(firstStr) && CurrencyCode::IsValid(firstStr)) { - // disambiguate currency code from exchange name - curStr = firstStr; - _pos += curStr.size(); - if (isCommaPresent) { - ++_pos; - } - } else if (fieldIs == FieldIs::kMandatory) { - throw invalid_argument("Expected a valid currency code in '{}'", std::string_view(_opt.begin() + _pos, _opt.end())); - } - return curStr; -} - -// At the end of the market, either the end of the string or a comma is expected. -Market StringOptionParser::parseMarket(FieldIs fieldIs) { - const std::size_t commaPos = _opt.find(',', _pos); - const auto begIt = _opt.begin() + _pos; - const bool isCommaPresent = commaPos != std::string_view::npos; - const std::string_view marketStr(begIt, isCommaPresent ? begIt + commaPos : _opt.end()); - const std::size_t dashPos = marketStr.find('-'); - - if (dashPos == std::string_view::npos) { - if (fieldIs == FieldIs::kMandatory) { - throw invalid_argument("Expected a dash in '{}'", std::string_view(_opt.begin() + _pos, _opt.end())); - } - return {}; - } - std::string_view firstCur(marketStr.begin(), marketStr.begin() + dashPos); - if (!CurrencyCode::IsValid(firstCur)) { - if (fieldIs == FieldIs::kMandatory) { - throw invalid_argument("Expected a valid first currency in '{}'", - std::string_view(_opt.begin() + _pos, _opt.end())); - } - return {}; - } - std::string_view secondCur(marketStr.begin() + dashPos + 1, marketStr.end()); - if (!CurrencyCode::IsValid(secondCur)) { - if (fieldIs == FieldIs::kMandatory) { - throw invalid_argument("Expected a valid second currency in '{}'", - std::string_view(_opt.begin() + _pos, _opt.end())); - } - return {}; - } - _pos += marketStr.size(); - if (isCommaPresent) { - ++_pos; - } - return {CurrencyCode(firstCur), CurrencyCode(secondCur)}; -} - // At the end of the currency, either the end of the string, or a dash or comma is expected. std::pair StringOptionParser::parseNonZeroAmount(FieldIs fieldIs) { - constexpr std::string_view sepWithPercentageAtLast = "-,%"; - std::size_t originalPos = _pos; + static constexpr std::string_view sepWithPercentageAtLast = "-,%"; + const auto originalPos = _pos; auto amountStr = GetNextStr(_opt, sepWithPercentageAtLast, _pos); - std::pair ret{MonetaryAmount(), StringOptionParser::AmountType::kNotPresent}; + auto ret = std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent); if (amountStr.empty()) { if (_pos == _opt.size()) { if (fieldIs == FieldIs::kMandatory) { @@ -179,8 +162,8 @@ ExchangeNames StringOptionParser::parseExchanges(char exchangesSep, char endExch std::string_view str(_opt.begin() + _pos, _opt.begin() + endPos); ExchangeNames exchanges; if (!str.empty()) { - std::size_t first = 0; - std::size_t last = str.find(exchangesSep); + auto last = str.find(exchangesSep); + decltype(last) first = 0; for (; last != std::string_view::npos; last = str.find(exchangesSep, last + 1)) { std::string_view exchangeNameStr(str.begin() + first, str.begin() + last); if (!exchangeNameStr.empty()) { diff --git a/src/http-request/include/curloptions.hpp b/src/http-request/include/curloptions.hpp index 3ddf3742..05cc391e 100644 --- a/src/http-request/include/curloptions.hpp +++ b/src/http-request/include/curloptions.hpp @@ -59,8 +59,8 @@ class CurlOptions { void setHttpHeader(std::string_view key, std::string_view value) { _httpHeaders.set(key, value); } void setHttpHeader(std::string_view key, std::integral auto value) { _httpHeaders.set(key, value); } - using trivially_relocatable = std::integral_constant && - is_trivially_relocatable_v>::type; + using trivially_relocatable = + std::bool_constant && is_trivially_relocatable_v>::type; private: void setPostDataInJsonFormat() { diff --git a/src/http-request/src/curlhandle.cpp b/src/http-request/src/curlhandle.cpp index d04f6065..02ce8281 100644 --- a/src/http-request/src/curlhandle.cpp +++ b/src/http-request/src/curlhandle.cpp @@ -264,6 +264,7 @@ std::string_view CurlHandle::query(std::string_view endpoint, const CurlOptions std::this_thread::sleep_for(sleepingTime); sleepingTime *= 2; } + auto t1 = Clock::now(); // Call diff --git a/src/objects/include/currencycode.hpp b/src/objects/include/currencycode.hpp index 51d1d9ca..dbfb4983 100644 --- a/src/objects/include/currencycode.hpp +++ b/src/objects/include/currencycode.hpp @@ -40,7 +40,7 @@ struct CurrencyCodeBase { kFirstAuthorizedLetter; } - static constexpr uint64_t DecimalsMask(bool isLongCurrencyCode) { + static constexpr uint64_t DecimalsMask(bool isLongCurrencyCode) noexcept { return isLongCurrencyCode ? kNbDecimals4Mask : kNbDecimals6Mask; } @@ -248,15 +248,15 @@ class CurrencyCode { explicit constexpr CurrencyCode(uint64_t data) : _data(data) {} - constexpr bool isLongCurrencyCode() const { return static_cast(_data & CurrencyCodeBase::kBeforeLastCharMask); } + constexpr bool isLongCurrencyCode() const noexcept { return (_data & CurrencyCodeBase::kBeforeLastCharMask) != 0; } - constexpr void uncheckedSetAdditionalBits(int8_t data) { + constexpr void uncheckedSetAdditionalBits(int8_t data) noexcept { // For currency codes whose length is > 8, only 15 digits are supported // max 64 decimals for currency codes whose length is maximum 8 (most cases) _data = static_cast(data) + (_data & (~CurrencyCodeBase::DecimalsMask(isLongCurrencyCode()))); } - constexpr int8_t getAdditionalBits() const { + constexpr int8_t getAdditionalBits() const noexcept { return static_cast(_data & CurrencyCodeBase::DecimalsMask(isLongCurrencyCode())); } diff --git a/src/objects/include/exchangesecretsinfo.hpp b/src/objects/include/exchangesecretsinfo.hpp index c79328b8..7ab7d4ba 100644 --- a/src/objects/include/exchangesecretsinfo.hpp +++ b/src/objects/include/exchangesecretsinfo.hpp @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "exchange-names.hpp" namespace cct { diff --git a/src/objects/include/market.hpp b/src/objects/include/market.hpp index b2fa5aae..c59d55a4 100644 --- a/src/objects/include/market.hpp +++ b/src/objects/include/market.hpp @@ -57,7 +57,7 @@ class Market { string str() const { return assetsPairStrUpper('-'); } - Type type() const { return static_cast(_assets[0].getAdditionalBits()); } + Type type() const noexcept { return static_cast(_assets[0].getAdditionalBits()); } friend std::ostream& operator<<(std::ostream& os, const Market& mk); diff --git a/src/objects/include/marketorderbook.hpp b/src/objects/include/marketorderbook.hpp index b4e26e99..0fbabfda 100644 --- a/src/objects/include/marketorderbook.hpp +++ b/src/objects/include/marketorderbook.hpp @@ -31,8 +31,7 @@ class MarketOrderBook { /// Constructs a new MarketOrderBook given a market and a list of amounts and prices. /// @param volAndPriNbDecimals optional to force number of decimals of amounts - explicit MarketOrderBook(TimePoint timeStamp, Market market, - const MarketOrderBookLines& orderLines = MarketOrderBookLines(), + explicit MarketOrderBook(TimePoint timeStamp, Market market, const MarketOrderBookLines& orderLines, VolAndPriNbDecimals volAndPriNbDecimals = VolAndPriNbDecimals()); /// Constructs a MarketOrderBook based on simple ticker information and price / amount precision @@ -213,8 +212,8 @@ class MarketOrderBook { TimePoint _time; Market _market; AmountPriceVector _orders; - int32_t _highestBidPricePos = 0; - int32_t _lowestAskPricePos = 0; + int32_t _highestBidPricePos{}; + int32_t _lowestAskPricePos{}; bool _isArtificiallyExtended = false; VolAndPriNbDecimals _volAndPriNbDecimals; }; diff --git a/src/objects/include/monetaryamount.hpp b/src/objects/include/monetaryamount.hpp index f4e79bd3..be6c036d 100644 --- a/src/objects/include/monetaryamount.hpp +++ b/src/objects/include/monetaryamount.hpp @@ -22,7 +22,7 @@ namespace cct { /// Represents a fixed-precision decimal amount with a CurrencyCode (fiat or coin). /// It is designed to be /// - fast -/// - small (16 bytes only). Thus can be passed by copy instead of reference +/// - small (16 bytes only). Thus can be passed by copy instead of reference (it is trivially copyable) /// - precise (amount is stored in a int64_t) /// - optimized, predictive and exact for additions and subtractions (if no overflow during the operation) /// @@ -33,7 +33,8 @@ namespace cct { /// - A CurrencyCode holding up to 10 chars + the number of decimals /// /// It can support up to 17 decimals for currency codes whose length is less than 9, -/// and up to 15 decimals for currencies whose length is 9 or 10. +/// and up to 15 decimals for currencies whose length is 9 or 10. Note that it's not possible to +/// store positive powers of 10 (only decimals, so negative powers of 10 are possible). /// /// Examples: $50, -2.045 BTC. /// The integral value stored in the MonetaryAmount is multiplied by 10^'_nbDecimals' @@ -110,7 +111,7 @@ class MonetaryAmount { [[nodiscard]] std::optional amount(int8_t nbDecimals) const; /// Get the integer part of the amount of this MonetaryAmount. - [[nodiscard]] constexpr AmountType integerPart() const { + [[nodiscard]] constexpr AmountType integerPart() const noexcept { return _amount / ipow10(static_cast(nbDecimals())); } @@ -134,9 +135,9 @@ class MonetaryAmount { return _curWithDecimals.withNoDecimalsPart(); } - [[nodiscard]] constexpr int8_t nbDecimals() const { return _curWithDecimals.getAdditionalBits(); } + [[nodiscard]] constexpr int8_t nbDecimals() const noexcept { return _curWithDecimals.getAdditionalBits(); } - [[nodiscard]] constexpr int8_t maxNbDecimals() const { + [[nodiscard]] constexpr int8_t maxNbDecimals() const noexcept { return _curWithDecimals.isLongCurrencyCode() ? CurrencyCodeBase::kMaxNbDecimalsLongCurrencyCode : std::numeric_limits::digits10 - 1; // -1 as minimal nb digits of integral part @@ -146,7 +147,7 @@ class MonetaryAmount { /// Examples: /// 0.00426622338114037 EUR -> 17 /// 45.546675 EUR -> 16 - [[nodiscard]] constexpr int8_t currentMaxNbDecimals() const { + [[nodiscard]] constexpr int8_t currentMaxNbDecimals() const noexcept { return static_cast(maxNbDecimals() - ndigits(integerPart()) + 1); } @@ -168,22 +169,22 @@ class MonetaryAmount { [[nodiscard]] std::strong_ordering operator<=>(const MonetaryAmount &other) const; - [[nodiscard]] constexpr bool operator==(const MonetaryAmount &) const = default; + [[nodiscard]] constexpr bool operator==(const MonetaryAmount &) const noexcept = default; /// Note: for comparison with numbers (integrals or double), only the amount is compared. /// To be consistent with operator<=>, the currency will be ignored for equality. /// TODO: check if this special behavior be problematic in some cases - [[nodiscard]] constexpr bool operator==(std::signed_integral auto amount) const { + [[nodiscard]] constexpr bool operator==(std::signed_integral auto amount) const noexcept { return _amount == static_cast(amount) && nbDecimals() == 0; } - [[nodiscard]] constexpr bool operator==(double amount) const { return amount == toDouble(); } + [[nodiscard]] constexpr bool operator==(double amount) const noexcept { return amount == toDouble(); } [[nodiscard]] constexpr auto operator<=>(std::signed_integral auto amount) const { return _amount <=> static_cast(amount) * ipow10(static_cast(nbDecimals())); } - [[nodiscard]] constexpr auto operator<=>(double amount) const { return toDouble() <=> amount; } + [[nodiscard]] constexpr auto operator<=>(double amount) const noexcept { return toDouble() <=> amount; } [[nodiscard]] constexpr MonetaryAmount abs() const noexcept { return {true, _amount < 0 ? -_amount : _amount, _curWithDecimals}; diff --git a/src/objects/include/order-book-line.hpp b/src/objects/include/order-book-line.hpp index 4c8caa9d..02e4e724 100644 --- a/src/objects/include/order-book-line.hpp +++ b/src/objects/include/order-book-line.hpp @@ -24,8 +24,6 @@ class OrderBookLine { MonetaryAmount price() const { return _amountPrice.price; } private: - friend class MarketOrderBook; - AmountPrice _amountPrice; }; diff --git a/src/objects/src/marketorderbook.cpp b/src/objects/src/marketorderbook.cpp index eb3f5b4b..10e8be56 100644 --- a/src/objects/src/marketorderbook.cpp +++ b/src/objects/src/marketorderbook.cpp @@ -18,6 +18,7 @@ #include "market.hpp" #include "monetaryamount.hpp" #include "order-book-line.hpp" +#include "overflow-check.hpp" #include "priceoptions.hpp" #include "priceoptionsdef.hpp" #include "simpletable.hpp" @@ -67,24 +68,32 @@ MarketOrderBook::MarketOrderBook(TimePoint timeStamp, Market market, const Marke std::ranges::sort(_orders, [](auto lhs, auto rhs) { return lhs.price < rhs.price; }); - for (auto it = _orders.begin(); it != _orders.end();) { - it = std::adjacent_find(it, _orders.end(), [](auto lhs, auto rhs) { return lhs.price == rhs.price; }); - if (it != _orders.end()) { - auto nextIt = std::next(it); - log::warn("Forbidden duplicate price {} at amounts {} & {} in the order book for market {}, summing them", - it->price, it->amount, nextIt->amount, market); + auto it = _orders.begin(); + while ((it = std::adjacent_find(it, _orders.end(), [](auto lhs, auto rhs) { return lhs.price == rhs.price; })) != + _orders.end()) { + auto nextIt = std::next(it); + log::warn("Forbidden duplicate price {} at amounts {} & {} in the order book for market {}, merging them", + it->price, it->amount, nextIt->amount, market); + if (WillSumOverflow(nextIt->amount, it->amount)) { + nextIt->amount = std::midpoint(nextIt->amount, it->amount); + } else { nextIt->amount += it->amount; - // Remove the first duplicated price line (we summed the amounts on the next line) - it = _orders.erase(it); - if (it->amount == 0) { - // If the sum has 0 amount, remove the next one as well - it = _orders.erase(it); - } } + // Remove the first duplicated price line (we summed the amounts on the next line) + if (nextIt->amount == 0) { + // If the sum has 0 amount, remove the next one as well + _orders.erase(it, std::next(nextIt)); + } else { + _orders.erase(it); + } + } + + const auto positiveAmounts = [](auto amountPrice) { return amountPrice.amount > 0; }; + if (!std::ranges::is_partitioned(_orders, positiveAmounts)) { + throw exception("Invalid market order book - check input data"); } - const auto highestBidPriceIt = - std::ranges::partition_point(_orders, [](auto amountPrice) { return amountPrice.amount > 0; }); + const auto highestBidPriceIt = std::ranges::partition_point(_orders, positiveAmounts); using PricePosT = decltype(_highestBidPricePos); diff --git a/src/objects/test/marketorderbook_test.cpp b/src/objects/test/marketorderbook_test.cpp index 3f39025c..6997c26d 100644 --- a/src/objects/test/marketorderbook_test.cpp +++ b/src/objects/test/marketorderbook_test.cpp @@ -59,6 +59,13 @@ TEST_F(MarketOrderBookTestCase1, NumberOfElements) { EXPECT_EQ(marketOrderBook.nbBidPrices(), 2); } +TEST_F(MarketOrderBookTestCase1, NbDecimals) { + const auto [volNbDecimals, priNbDecimals] = marketOrderBook.volAndPriNbDecimals(); + + EXPECT_EQ(volNbDecimals, 16); + EXPECT_EQ(priNbDecimals, 14); +} + TEST_F(MarketOrderBookTestCase1, MiddleElements) { EXPECT_EQ(marketOrderBook.lowestAskPrice(), MonetaryAmount("1302", "EUR")); EXPECT_EQ(marketOrderBook.highestBidPrice(), MonetaryAmount("1301", "EUR")); @@ -204,6 +211,29 @@ class MarketOrderBookTestCase2 : public ::testing::Test { OrderBookLine::Type::kBid)})}; }; +TEST_F(MarketOrderBookTestCase2, NbDecimals) { + const auto [volNbDecimals, priNbDecimals] = marketOrderBook.volAndPriNbDecimals(); + + EXPECT_EQ(volNbDecimals, 13); + EXPECT_EQ(priNbDecimals, 16); + + const int nbBids = marketOrderBook.nbBidPrices(); + for (int bidPos = 1; bidPos <= nbBids; ++bidPos) { + const auto [amount, price] = marketOrderBook[-bidPos]; + + EXPECT_NE(amount.amount(volNbDecimals), std::nullopt); + EXPECT_NE(price.amount(priNbDecimals), std::nullopt); + } + + const int nbAsks = marketOrderBook.nbAskPrices(); + for (int askPos = 1; askPos <= nbAsks; ++askPos) { + const auto [amount, price] = marketOrderBook[askPos]; + + EXPECT_NE(amount.amount(volNbDecimals), std::nullopt); + EXPECT_NE(price.amount(priNbDecimals), std::nullopt); + } +} + TEST_F(MarketOrderBookTestCase2, SimpleQueries) { EXPECT_EQ(marketOrderBook.size(), 9); EXPECT_EQ(marketOrderBook.lowestAskPrice(), MonetaryAmount("57.78", "KRW")); diff --git a/src/objects/test/monetaryamount_test.cpp b/src/objects/test/monetaryamount_test.cpp index d50686ed..bb801935 100644 --- a/src/objects/test/monetaryamount_test.cpp +++ b/src/objects/test/monetaryamount_test.cpp @@ -732,4 +732,19 @@ TEST(MonetaryAmountTest, CloseTo) { EXPECT_FALSE(MonetaryAmount("-0.90005").isCloseTo(MonetaryAmount("-0.9008"), 0.0001)); } +TEST(MonetaryAmountTest, CurrentMaxNbDecimals) { + MonetaryAmount ma1("0.00426622338114037 EUR"); + MonetaryAmount ma2("45.546675 EUR"); + MonetaryAmount ma3("3890.879"); + + EXPECT_EQ(ma1.currentMaxNbDecimals(), 17); + EXPECT_EQ(ma1.amount(ma1.currentMaxNbDecimals()), 426622338114037L); + + EXPECT_EQ(ma2.currentMaxNbDecimals(), 16); + EXPECT_EQ(ma2.amount(ma2.currentMaxNbDecimals()), 455466750000000000L); + + EXPECT_EQ(ma3.currentMaxNbDecimals(), 14); + EXPECT_EQ(ma3.amount(ma3.currentMaxNbDecimals()), 389087900000000000L); +} + } // namespace cct \ No newline at end of file diff --git a/src/tech/CMakeLists.txt b/src/tech/CMakeLists.txt index 25be8469..0e5909e9 100644 --- a/src/tech/CMakeLists.txt +++ b/src/tech/CMakeLists.txt @@ -90,6 +90,11 @@ add_unit_test( test/mathhelpers_test.cpp ) +add_unit_test( + overflow-check_test + test/overflow-check_test.cpp +) + add_unit_test( simpletable_test src/simpletable.cpp diff --git a/src/tech/include/mathhelpers.hpp b/src/tech/include/mathhelpers.hpp index 98314768..e084f427 100644 --- a/src/tech/include/mathhelpers.hpp +++ b/src/tech/include/mathhelpers.hpp @@ -67,7 +67,7 @@ constexpr int64_t ipow(int64_t base, uint8_t exp) noexcept { } } -/// Optimization of ipow10( exp) +/// Optimization of ipow(int64_t base, 10) constexpr int64_t ipow10(uint8_t exp) noexcept { constexpr const int64_t kPow10Table[] = {1LL, 10LL, diff --git a/src/tech/include/overflow-check.hpp b/src/tech/include/overflow-check.hpp new file mode 100644 index 00000000..a0c7782a --- /dev/null +++ b/src/tech/include/overflow-check.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +namespace cct { + +/// Simple utility function to check if performing the operator lhs + rhs (as signed integrals) would trigger an +/// overflow. +template +constexpr bool WillSumOverflow(T lhs, T rhs) { + if ((static_cast(lhs) ^ static_cast(rhs)) < 0) { + return false; + } + if (lhs > 0) { + return static_cast(rhs) > std::numeric_limits::max() - static_cast(lhs); + } + return static_cast(rhs) < std::numeric_limits::min() - static_cast(lhs); +} + +} // namespace cct \ No newline at end of file diff --git a/src/tech/test/overflow-check_test.cpp b/src/tech/test/overflow-check_test.cpp new file mode 100644 index 00000000..7b89a521 --- /dev/null +++ b/src/tech/test/overflow-check_test.cpp @@ -0,0 +1,18 @@ +#include "overflow-check.hpp" + +#include + +namespace cct { +TEST(WillSumOverflowTest, Int8) { + EXPECT_FALSE(WillSumOverflow(int8_t{}, int8_t{})); + EXPECT_FALSE(WillSumOverflow(int8_t{1}, int8_t{13})); + EXPECT_FALSE(WillSumOverflow(int8_t{-45}, int8_t{89})); + EXPECT_FALSE(WillSumOverflow(int8_t{-87}, int8_t{-25})); + EXPECT_FALSE(WillSumOverflow(int8_t{74}, int8_t{50})); + + EXPECT_TRUE(WillSumOverflow(int8_t{10}, int8_t{125})); + EXPECT_TRUE(WillSumOverflow(int8_t{-45}, int8_t{-89})); + EXPECT_TRUE(WillSumOverflow(int8_t{-87}, int8_t{-45})); + EXPECT_TRUE(WillSumOverflow(int8_t{74}, int8_t{60})); +} +} // namespace cct \ No newline at end of file