From abf3d6d4c188cd3cc4edb70ee2efa0db188b9d4b Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Tue, 10 Oct 2023 18:22:14 +0200 Subject: [PATCH] Implement CLI suggestions after errors --- .../include/commandlineoptionsparser.hpp | 27 ++++++++-- src/engine/src/coincentercommands.cpp | 1 - src/tech/CMakeLists.txt | 8 +++ .../include/levenshteindistancecalculator.hpp | 21 ++++++++ src/tech/include/stringhelpers.hpp | 6 +-- .../src/levenshteindistancecalculator.cpp | 40 +++++++++++++++ .../levenshteindistancecalculator_test.cpp | 50 +++++++++++++++++++ 7 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 src/tech/include/levenshteindistancecalculator.hpp create mode 100644 src/tech/src/levenshteindistancecalculator.cpp create mode 100644 src/tech/test/levenshteindistancecalculator_test.cpp diff --git a/src/engine/include/commandlineoptionsparser.hpp b/src/engine/include/commandlineoptionsparser.hpp index f0eaf9d9..074a1e43 100644 --- a/src/engine/include/commandlineoptionsparser.hpp +++ b/src/engine/include/commandlineoptionsparser.hpp @@ -15,6 +15,7 @@ #include "cct_vector.hpp" #include "commandlineoption.hpp" #include "durationstring.hpp" +#include "levenshteindistancecalculator.hpp" #include "stringhelpers.hpp" namespace cct { @@ -54,20 +55,26 @@ class CommandLineOptionsParser { } OptValueType parse(std::span groupedArguments) { - std::unordered_map callbacks; - callbacks.reserve(_opts.size()); + _callbacks.clear(); + _callbacks.reserve(_opts.size()); OptValueType data; for (const auto& [cmdLineOption, prop] : _opts) { - callbacks[cmdLineOption] = registerCallback(cmdLineOption, prop, data); + _callbacks[cmdLineOption] = registerCallback(cmdLineOption, prop, 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); })) { - throw invalid_argument("Unrecognized command-line option {}", argStr); + const auto [possibleOptionIdx, minDistance] = minLevenshteinDistanceOpt(argStr); + + if (minDistance <= 2) { + throw invalid_argument("Unrecognized command-line option '{}' - did you mean '{}'?", argStr, + _opts[possibleOptionIdx].first.fullName()); + } + throw invalid_argument("Unrecognized command-line option '{}'", argStr); } - for (auto& callback : callbacks) { + for (auto& callback : _callbacks) { callback.second(argPos, groupedArguments); } } @@ -254,7 +261,17 @@ class CommandLineOptionsParser { return lenFirstRows + 3; } + std::pair minLevenshteinDistanceOpt(std::string_view argStr) const { + vector minDistancesToFullNameOptions(_opts.size()); + LevenshteinDistanceCalculator calc; + std::ranges::transform(_opts, minDistancesToFullNameOptions.begin(), + [argStr, &calc](const auto opt) { return calc(opt.first.fullName(), argStr); }); + auto optIt = std::ranges::min_element(minDistancesToFullNameOptions); + return {optIt - minDistancesToFullNameOptions.begin(), *optIt}; + } + vector _opts; + std::unordered_map _callbacks; }; } // namespace cct diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index e25a465b..ba72640e 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -33,7 +33,6 @@ vector CoincenterCommands::ParseOptions(int argc, cons CoincenterCmdLineOptions globalOptions; // Support for command line multiple commands. Only full name flags are supported for multi command line commands. - // Note: maybe it better to just decommission short hand flags. while (parserIt.hasNext()) { std::span groupedArguments = parserIt.next(); diff --git a/src/tech/CMakeLists.txt b/src/tech/CMakeLists.txt index b3fd0631..264771fe 100644 --- a/src/tech/CMakeLists.txt +++ b/src/tech/CMakeLists.txt @@ -45,6 +45,14 @@ add_unit_test( CCT_DISABLE_SPDLOG ) +add_unit_test( + levenshteindistancecalculator_test + src/levenshteindistancecalculator.cpp + test/levenshteindistancecalculator_test.cpp + DEFINITIONS + CCT_DISABLE_SPDLOG +) + add_unit_test( flatkeyvaluestring_test test/flatkeyvaluestring_test.cpp diff --git a/src/tech/include/levenshteindistancecalculator.hpp b/src/tech/include/levenshteindistancecalculator.hpp new file mode 100644 index 00000000..05844e3f --- /dev/null +++ b/src/tech/include/levenshteindistancecalculator.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "cct_vector.hpp" + +namespace cct { +class LevenshteinDistanceCalculator { + public: + LevenshteinDistanceCalculator() noexcept = default; + + /// Computes the levenshtein distance between both input words. + /// Complexity is in 'word1.length() * word2.length()' in time, + /// min(word1.length(), word2.length()) in space. + int operator()(std::string_view word1, std::string_view word2); + + private: + // This is only for caching purposes, so that repeated calls to distance calculation do not allocate memory each time + vector _minDistance; +}; +} // namespace cct \ No newline at end of file diff --git a/src/tech/include/stringhelpers.hpp b/src/tech/include/stringhelpers.hpp index 8521474e..b26fe22d 100644 --- a/src/tech/include/stringhelpers.hpp +++ b/src/tech/include/stringhelpers.hpp @@ -1,9 +1,9 @@ #pragma once -#include - #include #include +#include +#include #include "cct_config.hpp" #include "cct_exception.hpp" @@ -14,7 +14,7 @@ namespace cct { namespace details { template -inline void ToChars(char *first, SizeType s, std::integral auto i) { +void ToChars(char *first, SizeType s, std::integral auto i) { if (auto [ptr, errc] = std::to_chars(first, first + s, i); CCT_UNLIKELY(errc != std::errc())) { throw exception("Unable to decode integral into string"); } diff --git a/src/tech/src/levenshteindistancecalculator.cpp b/src/tech/src/levenshteindistancecalculator.cpp new file mode 100644 index 00000000..bb5f29d5 --- /dev/null +++ b/src/tech/src/levenshteindistancecalculator.cpp @@ -0,0 +1,40 @@ +#include "levenshteindistancecalculator.hpp" + +#include +#include + +namespace cct { +int LevenshteinDistanceCalculator::operator()(std::string_view word1, std::string_view word2) { + if (word1.size() > word2.size()) { + std::swap(word1, word2); + } + + const int l1 = static_cast(word1.size()) + 1; + if (l1 > static_cast(_minDistance.size())) { + // Favor insert instead of resize to ensure reallocations are exponential + _minDistance.insert(_minDistance.end(), l1 - _minDistance.size(), 0); + } + + std::iota(_minDistance.begin(), _minDistance.end(), 0); + + const int l2 = static_cast(word2.size()) + 1; + for (int word2Pos = 1; word2Pos < l2; ++word2Pos) { + int previousDiagonal = _minDistance[0]; + + ++_minDistance[0]; + + for (int word1Pos = 1; word1Pos < l1; ++word1Pos) { + const int previousDiagonalSave = _minDistance[word1Pos]; + if (word1[word1Pos - 1] == word2[word2Pos - 1]) { + _minDistance[word1Pos] = previousDiagonal; + } else { + _minDistance[word1Pos] = + std::min(std::min(_minDistance[word1Pos - 1], _minDistance[word1Pos]), previousDiagonal) + 1; + } + previousDiagonal = previousDiagonalSave; + } + } + + return _minDistance[l1 - 1]; +} +} // namespace cct \ No newline at end of file diff --git a/src/tech/test/levenshteindistancecalculator_test.cpp b/src/tech/test/levenshteindistancecalculator_test.cpp new file mode 100644 index 00000000..1df81ee4 --- /dev/null +++ b/src/tech/test/levenshteindistancecalculator_test.cpp @@ -0,0 +1,50 @@ +#include "levenshteindistancecalculator.hpp" + +#include + +namespace cct { + +TEST(LevenshteinDistanceCalculator, CornerCases) { + LevenshteinDistanceCalculator calc; + + EXPECT_EQ(calc("", "tata"), 4); + EXPECT_EQ(calc("tutu", ""), 4); +} + +TEST(LevenshteinDistanceCalculator, SimpleCases) { + LevenshteinDistanceCalculator calc; + + EXPECT_EQ(calc("horse", "ros"), 3); + EXPECT_EQ(calc("intention", "execution"), 5); + EXPECT_EQ(calc("niche", "chien"), 4); +} + +TEST(LevenshteinDistanceCalculator, TypicalCases) { + LevenshteinDistanceCalculator calc; + + EXPECT_EQ(calc("--orderbook", "orderbook"), 2); + EXPECT_EQ(calc("--timeout-match", "--timeot-match"), 1); + EXPECT_EQ(calc("--no-multi-trade", "--no-mukti-trade"), 1); + EXPECT_EQ(calc("--updt-price", "--update-price"), 2); +} + +TEST(LevenshteinDistanceCalculator, ExtremeCases) { + LevenshteinDistanceCalculator calc; + + EXPECT_EQ( + calc( + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the " + "industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and " + "scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into " + "electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release " + "of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software " + "like Aldus PageMaker including versions of Lorem Ipsum.", + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the " + "industrj's standard dummytext ever since the 1500s, when an unknown printer took a galley of type and " + "scrambled iT to make a type specimen book. I has survived not only five centuroes, but also the leap into " + "electronic typesetting, i remaining essentially unchanged. It was popularised in the 1960s with the release " + "of Letraset sheets; containing Lorem Ipsum passages, and more recently with desktogp publishing software " + "like Aldus PageMaker including versions of Lorem Ipsum."), + 9); +} +} // namespace cct \ No newline at end of file