diff --git a/src/alphabets.rs b/src/alphabets.rs index 0afe1bb..e34e156 100644 --- a/src/alphabets.rs +++ b/src/alphabets.rs @@ -1,8 +1,7 @@ use crate::error; -const ALPHABETS: [(&'static str, &'static str); 22] = [ - ("numeric", "1234567890"), - ("abcd", "abcd"), +const ALPHABETS: [(&'static str, &'static str); 21] = [ + // ("abcd", "abcd"), ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), ("qwerty-homerow", "asdfjklgh"), ("qwerty-left-hand", "asdfqwerzcxv"), @@ -23,14 +22,45 @@ const ALPHABETS: [(&'static str, &'static str); 22] = [ ("colemak-homerow", "arstneiodh"), ("colemak-left-hand", "arstqwfpzxcv"), ("colemak-right-hand", "neioluymjhk"), + ( + "longest", + "aoeuqjkxpyhtnsgcrlmwvzfidb;,~<>'@!#$%^&*~1234567890", + ), ]; +/// Parse a name string into `Alphabet`, used during CLI parsing. +/// +/// # Note +/// +/// Letters 'n' and 'N' are systematically removed to prevent conflict with +/// navigation keys (arrows and 'n' 'N'). +pub fn parse_alphabet(src: &str) -> Result { + let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src); + + match alphabet_pair { + Some((_name, letters)) => { + let letters = letters.replace(&['n', 'N'][..], ""); + Ok(Alphabet(letters.to_string())) + } + None => Err(error::ParseError::UnknownAlphabet), + } +} + /// Type-safe string alphabet (newtype). #[derive(Debug)] pub struct Alphabet(pub String); impl Alphabet { - /// Create `n` hints. + /// Create `n` hints from the Alphabet. + /// + /// An Alphabet of `m` letters can produce at most `m^2` hints. In case + /// this limit is exceeded, this function will generate the `n` hints from + /// an Alphabet which has more letters (50). This will ensure 2500 hints + /// can be generated, which should cover all use cases (I think even + /// easymotion has less). + /// + /// If more hints are needed, unfortunately, this will keep producing + /// empty (`""`) hints. /// /// ``` /// // The algorithm works as follows: @@ -46,7 +76,19 @@ impl Alphabet { /// // pick a, generate aa ab ac ad | (a) (b) (c) (d) aa ab ac ad ba bb bc bd ca cb cc cd da db dc dd /// ``` pub fn make_hints(&self, n: usize) -> Vec { - let letters: Vec = self.0.chars().map(|s| s.to_string()).collect(); + // Shortcut if we have enough letters in the Alphabet. + if self.0.len() >= n { + return self.0.chars().take(n).map(|c| c.to_string()).collect(); + } + + // Use the "longest" alphabet if the current alphabet cannot produce as + // many hints as asked. + let letters: Vec = if self.0.len().pow(2) >= n { + self.0.chars().collect() + } else { + let alt_alphabet = parse_alphabet("longest").unwrap(); + alt_alphabet.0.chars().collect() + }; let mut lead = letters.clone(); let mut prev: Vec = Vec::new(); @@ -65,34 +107,23 @@ impl Alphabet { let gen: Vec = letters .iter() .take(n - lead.len() - prev.len()) - .map(|s| prefix.clone() + s) + .map(|c| format!("{}{}", prefix, c)) .collect(); // Insert gen in front of prev - prev.splice(0..0, gen); + prev.splice(..0, gen); } - lead = lead.iter().take(n - prev.len()).cloned().collect(); - lead.append(&mut prev); - lead - } -} + // Finalize by concatenating the lead and prev components, filling + // with "" as necessary. + let lead: Vec = lead.iter().map(|c| c.to_string()).collect(); -/// Parse a name string into `Alphabet`, supporting the CLI. -/// -/// # Note -/// -/// Letters 'n' and 'N' are systematically removed to prevent conflict with -/// navigation keys (arrows and 'n' 'N'). -pub fn parse_alphabet(src: &str) -> Result { - let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src); + let filler: Vec = std::iter::repeat("") + .take(n - lead.len() - prev.len()) + .map(|s| s.to_string()) + .collect(); - match alphabet_pair { - Some((_name, letters)) => { - let letters = letters.replace(&['n', 'N'][..], ""); - Ok(Alphabet(letters.to_string())) - } - None => Err(error::ParseError::UnknownAlphabet), + [lead, prev, filler].concat() } } @@ -124,7 +155,7 @@ mod tests { #[test] fn composed_matches_max_2() { let alphabet = Alphabet("ab".to_string()); - let hints = alphabet.make_hints(8); + let hints = alphabet.make_hints(4); assert_eq!(hints, ["aa", "ab", "ba", "bb"]); } @@ -136,6 +167,24 @@ mod tests { hints, ["a", "ba", "bb", "bc", "bd", "ca", "cb", "cc", "cd", "da", "db", "dc", "dd"] ); - // a (b) (c) (d) a ba bc bd ca cb cc cd da db dc dd + } + + #[test] + fn matches_with_longest_alphabet() { + let alphabet = Alphabet("ab".to_string()); + let hints = alphabet.make_hints(2500); + assert_eq!(hints.len(), 2500); + assert_eq!(&hints[..3], ["aa", "ao", "ae"]); + assert_eq!(&hints[2497..], ["08", "09", "00"]); + } + + #[test] + fn matches_exceed_longest_alphabet() { + let alphabet = Alphabet("ab".to_string()); + let hints = alphabet.make_hints(10000); + // 2500 unique hints are produced from the longest alphabet + // The 7500 last ones come from the filler ("" empty hints). + assert_eq!(hints.len(), 10000); + assert!(&hints[2500..].iter().all(|s| s == "")); } } diff --git a/src/lib.rs b/src/lib.rs index f716926..f7cc786 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,12 @@ pub struct CliOpt { /// Alphabet to draw hints from. /// /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", - /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", - /// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". + /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz" + /// "dvorak", "colemak". + /// + /// # Examples + /// + /// "qwerty", "dvorak-homerow", "azerty-right-hand". #[clap(short = "k", long, default_value = "dvorak", parse(try_from_str = alphabets::parse_alphabet))] alphabet: alphabets::Alphabet,