@@ -1,6 +1,7 @@ | |||
#include <dds/build/builder.hpp> | |||
#include <dds/catalog/catalog.hpp> | |||
#include <dds/catalog/get.hpp> | |||
#include <dds/dym.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/repo/repo.hpp> | |||
#include <dds/source/dist.hpp> | |||
@@ -197,11 +198,14 @@ struct cli_catalog { | |||
int run() { | |||
auto cat = cat_path.open(); | |||
for (const auto& req : requirements.Get()) { | |||
auto id = dds::package_id::parse(req); | |||
auto info = cat.get(id); | |||
auto id = dds::package_id::parse(req); | |||
dds::dym_target dym; | |||
auto info = cat.get(id); | |||
if (!info) { | |||
dds::throw_user_error<dds::errc::no_such_catalog_package>( | |||
"No package in the catalog matched the ID '{}'", req); | |||
"No package in the catalog matched the ID '{}'.{}", | |||
req, | |||
dym.sentence_suffix()); | |||
} | |||
auto tsd = dds::get_package_sdist(*info); | |||
auto out_path = out.Get(); |
@@ -1,5 +1,6 @@ | |||
#include "./catalog.hpp" | |||
#include <dds/dym.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/solve/solve.hpp> | |||
@@ -211,6 +212,12 @@ std::optional<package_info> catalog::get(const package_id& pk_id) const noexcept | |||
std::optional<std::string>, | |||
std::string>(st); | |||
if (!opt_tup) { | |||
dym_target::fill([&] { | |||
auto all_ids = this->all(); | |||
auto id_strings | |||
= ranges::views::transform(all_ids, [&](auto id) { return id.to_string(); }); | |||
return did_you_mean(pk_id.to_string(), id_strings); | |||
}); | |||
return std::nullopt; | |||
} | |||
const auto& [pkg_id, name, version, git_url, git_ref, lm_name, lm_namespace, description] |
@@ -0,0 +1,45 @@ | |||
#include <dds/dym.hpp> | |||
#include <range/v3/algorithm/min_element.hpp> | |||
#include <range/v3/view/cartesian_product.hpp> | |||
#include <range/v3/view/iota.hpp> | |||
#include <cassert> | |||
using namespace dds; | |||
thread_local dym_target* dym_target::_tls_current = nullptr; | |||
std::size_t dds::lev_edit_distance(std::string_view a, std::string_view b) noexcept { | |||
const auto n_rows = b.size() + 1; | |||
const auto n_columns = a.size() + 1; | |||
const auto empty_row = std::vector<std::size_t>(n_columns, 0); | |||
std::vector<std::vector<std::size_t>> matrix(n_rows, empty_row); | |||
auto row_iter = ranges::views::iota(1u, n_rows); | |||
auto col_iter = ranges::views::iota(1u, n_columns); | |||
for (auto n : col_iter) { | |||
matrix[0][n] = n; | |||
} | |||
for (auto n : row_iter) { | |||
matrix[n][0] = n; | |||
} | |||
auto prod = ranges::views::cartesian_product(row_iter, col_iter); | |||
for (auto [row, col] : prod) { | |||
auto cost = a[col - 1] == b[row - 1] ? 0 : 1; | |||
auto t1 = matrix[row - 1][col] + 1; | |||
auto t2 = matrix[row][col - 1] + 1; | |||
auto t3 = matrix[row - 1][col - 1] + cost; | |||
auto arr = std::array{t1, t2, t3}; | |||
matrix[row][col] = *ranges::min_element(arr); | |||
} | |||
return matrix.back().back(); | |||
} |
@@ -0,0 +1,61 @@ | |||
#pragma once | |||
#include <range/v3/algorithm/min_element.hpp> | |||
#include <range/v3/view/all.hpp> | |||
#include <optional> | |||
#include <string> | |||
#include <string_view> | |||
namespace dds { | |||
std::size_t lev_edit_distance(std::string_view a, std::string_view b) noexcept; | |||
class dym_target { | |||
std::optional<std::string> _candidate; | |||
dym_target* _tls_prev = nullptr; | |||
static thread_local dym_target* _tls_current; | |||
public: | |||
dym_target() | |||
: _tls_prev(_tls_current) { | |||
_tls_current = this; | |||
} | |||
dym_target(const dym_target&) = delete; | |||
~dym_target() { _tls_current = _tls_prev; } | |||
template <typename Func> | |||
static void fill(Func&& fn) noexcept { | |||
if (_tls_current) { | |||
_tls_current->_candidate = fn(); | |||
} | |||
} | |||
auto& candidate() const noexcept { return _candidate; } | |||
std::string sentence_suffix() const noexcept { | |||
if (_candidate) { | |||
return " (Did you mean '" + *_candidate + "'?)"; | |||
} | |||
return ""; | |||
} | |||
}; | |||
template <typename Range> | |||
std::optional<std::string> did_you_mean(std::string_view given, Range&& strings) noexcept { | |||
auto cand = ranges::min_element(strings, ranges::less{}, [&](std::string_view candidate) { | |||
return lev_edit_distance(candidate, given); | |||
}); | |||
if (cand == ranges::end(strings)) { | |||
return std::nullopt; | |||
} else { | |||
return std::string(*cand); | |||
} | |||
} | |||
inline std::optional<std::string> | |||
did_you_mean(std::string_view given, std::initializer_list<std::string_view> strings) noexcept { | |||
return did_you_mean(given, ranges::views::all(strings)); | |||
} | |||
} // namespace dds |
@@ -0,0 +1,16 @@ | |||
#include "./dym.hpp" | |||
#include <catch2/catch.hpp> | |||
TEST_CASE("Basic string distance") { | |||
CHECK(dds::lev_edit_distance("a", "a") == 0); | |||
CHECK(dds::lev_edit_distance("a", "b") == 1); | |||
CHECK(dds::lev_edit_distance("aa", "a") == 1); | |||
} | |||
TEST_CASE("Find the 'did-you-mean' candidate") { | |||
auto cand = dds::did_you_mean("food", {"foo", "bar"}); | |||
CHECK(cand == "foo"); | |||
cand = dds::did_you_mean("eatable", {"edible", "tangible"}); | |||
CHECK(cand == "edible"); | |||
} |