1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
//! Minimal library to provide clap-style "Did you mean?" suggestions.
//!
//! The implementation is copied directly from clap.
//!
//! ## Examples
//! ```rust
//! let possible_vals = vec!["test", "possible", "values"];
//! let input = "tst";
//! let suggestions = suggestions::provide_suggestions(input, &possible_vals);
//! assert_eq!(suggestions, vec!["test"]);
//! // We have a convenience function to only pick only a single suggestion, giving `Some` or `None`
//! let single_suggestion = suggestions::provide_a_suggestion(input, &possible_vals);
//! assert_eq!(single_suggestion.unwrap(), "test");
//! ```
//!
//! ### Multiple matches
//! Sometimes, there may be multiple (good) suggestions.
//!
//! Consider the following example:
//!
//! ```rust
//! let possible_vals = vec!["testing", "tempo"];
//! let input = "teso"; // Sems ambiguous. Maybe multiple suggestions?
//! let suggestions = suggestions::provide_suggestions(input, &possible_vals);
//! // The implementation trys to order matches from "best" to "wort"
//! assert_eq!(suggestions, vec!["testing", "tempo"]);
//! ```
//!
//! Asking for a single suggestion here (`provide_a_suggestion`) would attempt to return the "best" one.
//! As you can immagine, that may not be what the user expects.
//! Therefore, it is best to stick with `provide_suggesetions`.
//!
//! ### No matches
//! If nothing is reasonably similar, asking for suggestions
//! will return `vec![]` or `None`.
//!
//! ```rust
//! let possible_vals = vec!["testing", "things", "here"];
//! let input = "--something-completely_different";
//! assert_eq!(suggestions::provide_a_suggestion(&input, &possible_vals), None)
//! ```
//!
//! ## Binary
//! This crate also comes usable as an (optional) zero-dependency binary.
//!
//! See documentation at [the readme](https://github.com/Techcable/rust-suggestions#binary).
//!
//! At the time of this writing it is 276k ;)
#![deny(missing_docs)]
use std::cmp::Ordering;
use std::convert::AsRef;

/// The confidence interval used to detect similarities.
const CONFIDENCE_THRESHOLD: f64 = 0.8;

/// Suggest a string that most is similar to `target`,
/// but is actually present in the `possible_values`.
///
/// Returns `None` if there is nothing in `possible_values` that is reasonably similar.
///
/// See also [provide_suggestions] to consider multiple possible suggestions (**recommended**).
///
/// If there are multiple ,possible
pub fn provide_a_suggestion<I, T>(target: &str, possible_values: I) -> Option<String>
where
    T: AsRef<str>,
    I: IntoIterator<Item = T>,
{
    provide_suggestions(target, possible_values).pop()
}

/// Suggest strings that are similar to `target`,
/// but are actually present in the `possible_values`.
///
/// Returns an empty vector `vec![]` if there is nothing reasonbly similar.
///
/// The implementation sorts suggestions based on its internal notion of similarity (from best ->
/// worst).
///
/// See also [provide_a_suggestion], which only picks a single suggesetion.
pub fn provide_suggestions<I, T>(target: &str, possible_values: I) -> Vec<String>
where
    T: AsRef<str>,
    I: IntoIterator<Item = T>,
{
    // Implementation copied directly from clap
    // See here: https://github.com/clap-rs/clap/blob/7b7c76e3d0279b474c774ea738aecb1d77251df8/src/parse/features/suggestions.rs#L17-L23
    let mut candidates: Vec<(f64, String)> = possible_values
        .into_iter()
        .map(|pv| {
            (
                strsim::jaro_winkler(target, pv.as_ref()),
                pv.as_ref().into(),
            )
        })
        .filter(|(confidence, _)| *confidence > CONFIDENCE_THRESHOLD)
        .collect();
    candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
    candidates.into_iter().map(|(_, pv)| pv).collect()
}