cmStringAlgorithms: Add Levenshtein distance

This commit is contained in:
Tyler Yankee
2026-09-11 08:23:29 -04:00
parent b2e4ccebb4
commit fca1ecb8a3
3 changed files with 106 additions and 0 deletions
+68
View File
@@ -7,6 +7,7 @@
#include <cstddef> // IWYU pragma: keep
#include <cstdio>
#include <cstdlib>
#include <limits>
#include "cmsys/String.h"
@@ -86,6 +87,73 @@ std::string cmEscapeQuotes(cm::string_view str)
return result;
}
std::size_t cmLevenshteinDistance(cm::string_view a, cm::string_view b)
{
if (a == b) {
return 0;
}
std::size_t const aSize = a.size();
std::size_t const bSize = b.size();
if (aSize == 0) {
return bSize;
}
if (bSize == 0) {
return aSize;
}
std::vector<std::size_t> previous(bSize + 1, 0);
std::vector<std::size_t> current(bSize + 1, 0);
for (std::size_t j = 0; j <= bSize; ++j) {
previous[j] = j;
}
for (std::size_t i = 1; i <= aSize; ++i) {
current[0] = i;
for (std::size_t j = 1; j <= bSize; ++j) {
std::size_t const substitutionCost = (a[i - 1] == b[j - 1]) ? 0 : 1;
current[j] = std::min({ previous[j] + 1, current[j - 1] + 1,
previous[j - 1] + substitutionCost });
}
previous.swap(current);
}
return previous[bSize];
}
std::string cmFindClosestString(cm::string_view input,
std::vector<std::string> const& candidates)
{
if (candidates.empty()) {
return std::string();
}
std::string best;
std::size_t bestDistance = std::numeric_limits<std::size_t>::max();
for (std::string const& candidate : candidates) {
std::size_t const distance = cmLevenshteinDistance(input, candidate);
if (distance < bestDistance) {
bestDistance = distance;
best = candidate;
}
}
// Scale the acceptable edit distance with the input length (roughly one
// typo per 5 characters), but keep it within [kMinDistance, kMaxDistance]
// so very short inputs still allow one edit and very long inputs don't
// start accepting barely-related matches.
std::size_t const kMinDistance = 1;
std::size_t const kMaxDistance = 4;
std::size_t const maxDistance =
std::min(kMaxDistance, std::max(kMinDistance, input.size() / 5 + 1));
if (bestDistance > maxDistance) {
return std::string();
}
return best;
}
namespace {
template <std::size_t N, typename T>
inline void MakeDigits(cm::string_view& view, char (&digits)[N],
+7
View File
@@ -64,6 +64,13 @@ std::string cmRemoveQuotes(cm::string_view str);
/** Escape quotes in a string. */
std::string cmEscapeQuotes(cm::string_view str);
/** Compute the Levenshtein distance between two strings. */
std::size_t cmLevenshteinDistance(cm::string_view a, cm::string_view b);
/** Return the closest candidate string to the given input. */
std::string cmFindClosestString(cm::string_view input,
std::vector<std::string> const& candidates);
/** Joins elements of a range with separator into a single string. */
template <typename Range>
std::string cmJoin(Range const& rng, cm::string_view separator)
+31
View File
@@ -313,5 +313,36 @@ int testStringAlgorithms(int /*unused*/, char* /*unused*/[])
"cmStrLen returns length of empty literal string");
}
// ----------------------------------------------------------------------
// Test cmLevenshteinDistance
{
assert_ok(cmLevenshteinDistance("", "") == 0,
"cmLevenshteinDistance empty strings");
assert_ok(cmLevenshteinDistance("abc", "abc") == 0,
"cmLevenshteinDistance identical strings");
assert_ok(cmLevenshteinDistance("", "abc") == 3,
"cmLevenshteinDistance empty vs non-empty");
assert_ok(cmLevenshteinDistance("abc", "") == 3,
"cmLevenshteinDistance non-empty vs empty");
assert_ok(cmLevenshteinDistance("toolchain", "tolchain") == 1,
"cmLevenshteinDistance single deletion");
assert_ok(cmLevenshteinDistance("kitten", "sitting") == 3,
"cmLevenshteinDistance words");
}
// ----------------------------------------------------------------------
// Test cmFindClosestString
{
std::vector<std::string> const candidates = { "--build", "--install",
"--toolchain",
"--install-prefix" };
assert_string(cmFindClosestString("--toolchan", candidates), "--toolchain",
"cmFindClosestString finds close match");
assert_string(cmFindClosestString("--totally-unrelated", candidates), "",
"cmFindClosestString rejects distant strings");
assert_string(cmFindClosestString("anything", {}), "",
"cmFindClosestString empty candidates");
}
return failed;
}