diff --git a/src/libutil/suggestions.cc b/src/libutil/suggestions.cc index 96b48416b..bcd93aa6b 100644 --- a/src/libutil/suggestions.cc +++ b/src/libutil/suggestions.cc @@ -5,15 +5,8 @@ namespace nix { -/** - * Return `some(distance)` where distance is an integer representing some - * notion of distance between both arguments. - * - * If the distance is too big, return none - */ -int distanceBetween(std::string_view first, std::string_view second) +int levenshteinDistance(std::string_view first, std::string_view second) { - // Levenshtein distance. // Implementation borrowed from // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows @@ -49,7 +42,7 @@ Suggestions Suggestions::bestMatches ( std::set res; for (const auto & possibleMatch : allMatches) { res.insert(Suggestion { - .distance = distanceBetween(query, possibleMatch), + .distance = levenshteinDistance(query, possibleMatch), .suggestion = possibleMatch, }); } @@ -63,7 +56,7 @@ Suggestions Suggestions::trim(int limit, int maxDistance) const int count = 0; for (auto & elt : suggestions) { - if (count >= limit || elt.distance >= maxDistance) + if (count >= limit || elt.distance > maxDistance) break; count++; res.insert(elt); diff --git a/src/libutil/suggestions.hh b/src/libutil/suggestions.hh index 3caed487a..76063a261 100644 --- a/src/libutil/suggestions.hh +++ b/src/libutil/suggestions.hh @@ -6,6 +6,8 @@ namespace nix { +int levenshteinDistance(std::string_view first, std::string_view second); + /** * A potential suggestion for the cli interface. */ diff --git a/src/libutil/tests/suggestions.cc b/src/libutil/tests/suggestions.cc new file mode 100644 index 000000000..279994abc --- /dev/null +++ b/src/libutil/tests/suggestions.cc @@ -0,0 +1,43 @@ +#include "suggestions.hh" +#include + +namespace nix { + + struct LevenshteinDistanceParam { + std::string s1, s2; + int distance; + }; + + class LevenshteinDistanceTest : + public testing::TestWithParam { + }; + + TEST_P(LevenshteinDistanceTest, CorrectlyComputed) { + auto params = GetParam(); + + ASSERT_EQ(levenshteinDistance(params.s1, params.s2), params.distance); + ASSERT_EQ(levenshteinDistance(params.s2, params.s1), params.distance); + } + + INSTANTIATE_TEST_SUITE_P(LevenshteinDistance, LevenshteinDistanceTest, + testing::Values( + LevenshteinDistanceParam{"foo", "foo", 0}, + LevenshteinDistanceParam{"foo", "", 3}, + LevenshteinDistanceParam{"", "", 0}, + LevenshteinDistanceParam{"foo", "fo", 1}, + LevenshteinDistanceParam{"foo", "oo", 1}, + LevenshteinDistanceParam{"foo", "fao", 1}, + LevenshteinDistanceParam{"foo", "abc", 3} + ) + ); + + TEST(Suggestions, Trim) { + auto suggestions = Suggestions::bestMatches({"foooo", "bar", "fo", "gao"}, "foo"); + auto onlyOne = suggestions.trim(1); + ASSERT_EQ(onlyOne.suggestions.size(), 1); + ASSERT_TRUE(onlyOne.suggestions.begin()->suggestion == "fo"); + + auto closest = suggestions.trim(999, 2); + ASSERT_EQ(closest.suggestions.size(), 3); + } +}