Skip to content

Commit 6501eb6

Browse files
committed
feat: using levenshtein_distance() function to provide best matches for unknown commands & options (#511)
1 parent f90876a commit 6501eb6

File tree

1 file changed

+61
-2
lines changed

1 file changed

+61
-2
lines changed

libchess/src/uci/EngineBase.cpp

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414

1515
#include <algorithm>
16+
#include <array>
1617
#include <atomic>
1718
#include <cassert>
1819
#include <expected>
@@ -25,9 +26,11 @@
2526
#include <libchess/uci/Printing.hpp>
2627
#include <libutil/Strings.hpp>
2728
#include <print>
29+
#include <ranges>
2830
#include <string>
2931
#include <string_view>
3032
#include <utility>
33+
#include <vector>
3134

3235
namespace chess::uci {
3336

@@ -48,6 +51,29 @@ using util::strings::trim;
4851
// defined out-of-line to address -Wweak-vtables
4952
EngineBase::~EngineBase() = default;
5053

54+
namespace {
55+
// returns name of closest known command
56+
[[nodiscard]] auto find_nearest_command(
57+
const string_view input, const EngineBase::CommandList standardCommands, const EngineBase::CommandList customCommands)
58+
-> string_view
59+
{
60+
// map commands to pair of: command name, Levenshtein distance from input
61+
const auto mapped
62+
= std::views::join(std::array { standardCommands, customCommands })
63+
| std::views::transform([input](const EngineCommand& command) {
64+
return std::make_pair(
65+
command.name,
66+
util::strings::levenshtein_distance(input, command.name));
67+
})
68+
| std::ranges::to<std::vector>();
69+
70+
const auto closest = std::ranges::min(
71+
mapped, std::ranges::less { }, [](const auto& item) { return item.second; });
72+
73+
return closest.first;
74+
}
75+
} // namespace
76+
5177
void EngineBase::handle_command(const string_view command)
5278
{
5379
auto [firstWord, rest] = split_at_first_space(command);
@@ -72,7 +98,12 @@ void EngineBase::handle_command(const string_view command)
7298
return;
7399
}
74100

75-
info_string(std::format("Unknown UCI command: '{}'", firstWord));
101+
info_string(std::format(
102+
"Unknown UCI command: '{}'", firstWord));
103+
104+
info_string(std::format(
105+
"The closest known command is: {}",
106+
find_nearest_command(firstWord, standardUCICommands, customCommands)));
76107
}
77108

78109
void EngineBase::respond_to_uci()
@@ -154,6 +185,29 @@ void EngineBase::handle_setpos(const string_view arguments)
154185
});
155186
}
156187

188+
namespace {
189+
// returns name of closest known option
190+
[[nodiscard]] auto find_nearest_option(
191+
const string_view input, const EngineBase::OptionList standardOptions, const EngineBase::OptionList customOptions)
192+
-> string_view
193+
{
194+
// map options to pair of: option name, Levenshtein distance from input
195+
const auto mapped
196+
= std::views::join(std::array { standardOptions, customOptions })
197+
| std::views::transform([input](const Option* option) {
198+
return std::make_pair(
199+
option->get_name(),
200+
util::strings::levenshtein_distance(input, option->get_name()));
201+
})
202+
| std::ranges::to<std::vector>();
203+
204+
const auto closest = std::ranges::min(
205+
mapped, std::ranges::less { }, [](const auto& item) { return item.second; });
206+
207+
return closest.first;
208+
}
209+
} // namespace
210+
157211
void EngineBase::handle_setoption(const string_view arguments)
158212
{
159213
auto [firstWord, rest] = split_at_first_space(arguments);
@@ -205,7 +259,12 @@ void EngineBase::handle_setoption(const string_view arguments)
205259
if (update_option(get_custom_uci_options()))
206260
return;
207261

208-
info_string(std::format("Attempted to set unknown option '{}'", name));
262+
info_string(std::format(
263+
"Attempted to set unknown option '{}'", name));
264+
265+
info_string(std::format(
266+
"The closest known option is: {}",
267+
find_nearest_option(name, standardUCIOptions, get_custom_uci_options())));
209268
}
210269

211270
void EngineBase::loop()

0 commit comments

Comments
 (0)