Skip to content

Commit

Permalink
Implement CLI suggestions after errors
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanel committed Oct 10, 2023
1 parent f8eae33 commit abf3d6d
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 @@ -54,20 +55,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 @@ -254,7 +261,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 abf3d6d

Please sign in to comment.