Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions libchess/src/uci/EngineBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <expected>
Expand All @@ -25,9 +26,11 @@
#include <libchess/uci/Printing.hpp>
#include <libutil/Strings.hpp>
#include <print>
#include <ranges>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace chess::uci {

Expand All @@ -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<std::vector>();

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);
Expand All @@ -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()
Expand Down Expand Up @@ -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<std::vector>();

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);
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions libutil/include/libutil/Strings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// @}

/*
Expand Down
28 changes: 28 additions & 0 deletions libutil/src/Strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <ranges>
#include <string>
#include <string_view>
#include <vector>

namespace {

Expand Down Expand Up @@ -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<std::vector>();

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
17 changes: 17 additions & 0 deletions tests/unit/libutil/Strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading