Skip to content

Commit

Permalink
Implement CLI suggestions in case of errors thanks to Levenshtein dis…
Browse files Browse the repository at this point in the history
…tance to all option names computation
  • Loading branch information
sjanel committed Oct 11, 2023
1 parent f7ff9c4 commit 8c564e3
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 9 deletions.
27 changes: 22 additions & 5 deletions src/engine/include/commandlineoptionsparser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "cct_vector.hpp"
#include "commandlineoption.hpp"
#include "durationstring.hpp"
#include "levenshteindistancecalculator.hpp"
#include "stringhelpers.hpp"

namespace cct {
Expand Down Expand Up @@ -53,20 +54,26 @@ class CommandLineOptionsParser {
}

OptValueType parse(std::span<const char*> groupedArguments) {
std::unordered_map<CommandLineOption, CallbackType> 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<int>(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);
}
}
Expand Down Expand Up @@ -250,7 +257,17 @@ class CommandLineOptionsParser {
return lenFirstRows + 3;
}

std::pair<int, int> minLevenshteinDistanceOpt(std::string_view argStr) const {
vector<int> 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<CommandLineOptionWithValue> _opts;
std::unordered_map<CommandLineOption, CallbackType> _callbacks;
};

} // namespace cct
1 change: 0 additions & 1 deletion src/engine/src/coincentercommands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ vector<CoincenterCmdLineOptions> 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<const char *> groupedArguments = parserIt.next();

Expand Down
8 changes: 8 additions & 0 deletions src/tech/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/tech/include/levenshteindistancecalculator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <string_view>

#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<int> _minDistance;
};
} // namespace cct
6 changes: 3 additions & 3 deletions src/tech/include/stringhelpers.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#pragma once

#include <string.h>

#include <charconv>
#include <concepts>
#include <cstring>
#include <string_view>

#include "cct_config.hpp"
#include "cct_exception.hpp"
Expand All @@ -14,7 +14,7 @@ namespace cct {

namespace details {
template <class SizeType>
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");
}
Expand Down
40 changes: 40 additions & 0 deletions src/tech/src/levenshteindistancecalculator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "levenshteindistancecalculator.hpp"

#include <algorithm>
#include <numeric>

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<int>(word1.size()) + 1;
if (l1 > static_cast<int>(_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<int>(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
50 changes: 50 additions & 0 deletions src/tech/test/levenshteindistancecalculator_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include "levenshteindistancecalculator.hpp"

#include <gtest/gtest.h>

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

0 comments on commit 8c564e3

Please sign in to comment.