diff --git a/libchess/src/uci/EngineBase.cpp b/libchess/src/uci/EngineBase.cpp index 09230b9a..fd02678e 100644 --- a/libchess/src/uci/EngineBase.cpp +++ b/libchess/src/uci/EngineBase.cpp @@ -13,6 +13,7 @@ */ #include +#include #include #include #include @@ -25,9 +26,11 @@ #include #include #include +#include #include #include #include +#include namespace chess::uci { @@ -48,6 +51,29 @@ using util::strings::trim; // defined out-of-line to address -Wweak-vtables EngineBase::~EngineBase() = default; +namespace { + // returns name of closest known command + [[nodiscard]] auto find_nearest_command( + const string_view input, const EngineBase::CommandList standardCommands, const EngineBase::CommandList customCommands) + -> string_view + { + // map commands to pair of: command name, Levenshtein distance from input + const auto mapped + = std::views::join(std::array { standardCommands, customCommands }) + | std::views::transform([input](const EngineCommand& command) { + return std::make_pair( + command.name, + util::strings::levenshtein_distance(input, command.name)); + }) + | std::ranges::to(); + + const auto closest = std::ranges::min( + mapped, std::ranges::less { }, [](const auto& item) { return item.second; }); + + return closest.first; + } +} // namespace + void EngineBase::handle_command(const string_view command) { auto [firstWord, rest] = split_at_first_space(command); @@ -72,7 +98,12 @@ void EngineBase::handle_command(const string_view command) return; } - info_string(std::format("Unknown UCI command: '{}'", firstWord)); + info_string(std::format( + "Unknown UCI command: '{}'", firstWord)); + + info_string(std::format( + "The closest known command is: {}", + find_nearest_command(firstWord, standardUCICommands, customCommands))); } void EngineBase::respond_to_uci() @@ -154,6 +185,29 @@ void EngineBase::handle_setpos(const string_view arguments) }); } +namespace { + // returns name of closest known option + [[nodiscard]] auto find_nearest_option( + const string_view input, const EngineBase::OptionList standardOptions, const EngineBase::OptionList customOptions) + -> string_view + { + // map options to pair of: option name, Levenshtein distance from input + const auto mapped + = std::views::join(std::array { standardOptions, customOptions }) + | std::views::transform([input](const Option* option) { + return std::make_pair( + option->get_name(), + util::strings::levenshtein_distance(input, option->get_name())); + }) + | std::ranges::to(); + + const auto closest = std::ranges::min( + mapped, std::ranges::less { }, [](const auto& item) { return item.second; }); + + return closest.first; + } +} // namespace + void EngineBase::handle_setoption(const string_view arguments) { auto [firstWord, rest] = split_at_first_space(arguments); @@ -205,7 +259,12 @@ void EngineBase::handle_setoption(const string_view arguments) if (update_option(get_custom_uci_options())) return; - info_string(std::format("Attempted to set unknown option '{}'", name)); + info_string(std::format( + "Attempted to set unknown option '{}'", name)); + + info_string(std::format( + "The closest known option is: {}", + find_nearest_option(name, standardUCIOptions, get_custom_uci_options()))); } void EngineBase::loop() diff --git a/libutil/include/libutil/Strings.hpp b/libutil/include/libutil/Strings.hpp index 6607d6f9..60801423 100644 --- a/libutil/include/libutil/Strings.hpp +++ b/libutil/include/libutil/Strings.hpp @@ -130,6 +130,10 @@ void write_integer( */ [[nodiscard]] auto words_view(string_view text); +/** Computes the Levenshtein distance between the two strings. */ +[[nodiscard, gnu::const]] auto levenshtein_distance( + string_view first, string_view second) -> size_t; + /// @} /* diff --git a/libutil/src/Strings.cpp b/libutil/src/Strings.cpp index 6bd66dce..42f28161 100644 --- a/libutil/src/Strings.cpp +++ b/libutil/src/Strings.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace { @@ -150,4 +151,31 @@ auto split_at_first_space_or_newline(const string_view input) -> StringViewPair }; } +auto levenshtein_distance( + const string_view first, const string_view second) -> size_t +{ + const auto size_a = first.size(); + const auto size_b = second.size(); + + auto distances = std::views::iota(0uz, size_b + 1uz) + | std::ranges::to(); + + for (auto i = 0uz; i < size_a; ++i) { + auto prevDist = 0uz; + + for (auto j = 0uz; j < size_b; ++j) { + const auto next = distances.at(j + 1uz); + + const auto dist = std::exchange(prevDist, next) + + (first.at(i) == second.at(j) ? 0uz : 1uz); + + distances.at(j + 1uz) = std::min({ dist, + distances.at(j) + 1uz, + next + 1uz }); + } + } + + return distances.at(size_b); +} + } // namespace util::strings diff --git a/tests/unit/libutil/Strings.cpp b/tests/unit/libutil/Strings.cpp index aee30a9b..34b5645b 100644 --- a/tests/unit/libutil/Strings.cpp +++ b/tests/unit/libutil/Strings.cpp @@ -219,3 +219,20 @@ TEST_CASE("Strings - words_view()", TAGS) REQUIRE(words.back() == "456"); } } + +TEST_CASE("Strings - Levenshtein distance", TAGS) +{ + using util::strings::levenshtein_distance; + + REQUIRE( + levenshtein_distance("kitten", "sitting") == 3uz); + + REQUIRE( + levenshtein_distance("corporate", "cooperation") == 5uz); + + REQUIRE( + levenshtein_distance("123", { }) == 0uz); + + REQUIRE( + levenshtein_distance({ }, { }) == 0uz); +}