mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
cmStringAlgorithms: Add Levenshtein distance
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user