From 53f7084303554d78c4ae023bd62d9642153932ab Mon Sep 17 00:00:00 2001 From: graelo Date: Sat, 30 May 2020 19:28:54 +0200 Subject: [PATCH] refactor: wip alphabet & regexes --- src/alphabets.rs | 203 ++++++++++++++++++++++++++--------------------- src/lib.rs | 2 +- src/state.rs | 13 ++- src/view.rs | 3 + 4 files changed, 124 insertions(+), 97 deletions(-) diff --git a/src/alphabets.rs b/src/alphabets.rs index 7d9897c..0afe1bb 100644 --- a/src/alphabets.rs +++ b/src/alphabets.rs @@ -1,116 +1,141 @@ use crate::error; const ALPHABETS: [(&'static str, &'static str); 22] = [ - ("numeric", "1234567890"), - ("abcd", "abcd"), - ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), - ("qwerty-homerow", "asdfjklgh"), - ("qwerty-left-hand", "asdfqwerzcxv"), - ("qwerty-right-hand", "jkluiopmyhn"), - ("azerty", "qsdfazerwxcvjklmuiopghtybn"), - ("azerty-homerow", "qsdfjkmgh"), - ("azerty-left-hand", "qsdfazerwxcv"), - ("azerty-right-hand", "jklmuiophyn"), - ("qwertz", "asdfqweryxcvjkluiopmghtzbn"), - ("qwertz-homerow", "asdfghjkl"), - ("qwertz-left-hand", "asdfqweryxcv"), - ("qwertz-right-hand", "jkluiopmhzn"), - ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"), - ("dvorak-homerow", "aoeuhtnsid"), - ("dvorak-left-hand", "aoeupqjkyix"), - ("dvorak-right-hand", "htnsgcrlmwvz"), - ("colemak", "arstqwfpzxcvneioluymdhgjbk"), - ("colemak-homerow", "arstneiodh"), - ("colemak-left-hand", "arstqwfpzxcv"), - ("colemak-right-hand", "neioluymjhk"), + ("numeric", "1234567890"), + ("abcd", "abcd"), + ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), + ("qwerty-homerow", "asdfjklgh"), + ("qwerty-left-hand", "asdfqwerzcxv"), + ("qwerty-right-hand", "jkluiopmyhn"), + ("azerty", "qsdfazerwxcvjklmuiopghtybn"), + ("azerty-homerow", "qsdfjkmgh"), + ("azerty-left-hand", "qsdfazerwxcv"), + ("azerty-right-hand", "jklmuiophyn"), + ("qwertz", "asdfqweryxcvjkluiopmghtzbn"), + ("qwertz-homerow", "asdfghjkl"), + ("qwertz-left-hand", "asdfqweryxcv"), + ("qwertz-right-hand", "jkluiopmhzn"), + ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"), + ("dvorak-homerow", "aoeuhtnsid"), + ("dvorak-left-hand", "aoeupqjkyix"), + ("dvorak-right-hand", "htnsgcrlmwvz"), + ("colemak", "arstqwfpzxcvneioluymdhgjbk"), + ("colemak-homerow", "arstneiodh"), + ("colemak-left-hand", "arstqwfpzxcv"), + ("colemak-right-hand", "neioluymjhk"), ]; -// pub struct Alphabet<'a> { -// letters: &'a str, -// } - /// Type-safe string alphabet (newtype). #[derive(Debug)] pub struct Alphabet(pub String); impl Alphabet { - pub fn hints(&self, matches: usize) -> Vec { - let letters: Vec = self.0.chars().map(|s| s.to_string()).collect(); + /// Create `n` hints. + /// + /// ``` + /// // The algorithm works as follows: + /// // --- lead ---- + /// // initial state | a b c d + /// + /// // along as we need more hints, and still have capacity, do the following + /// + /// // --- lead ---- --- gen --- -------------- prev --------------- + /// // pick d, generate da db dc dd | a b c (d) da db dc dd + /// // pick c, generate ca cb cc cd | a b (c) (d) ca cb cc cd da db dc dd + /// // pick b, generate ba bb bc bd | a (b) (c) (d) ba bb bc bd ca cb cc cd da db dc dd + /// // 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(); - let mut expansion = letters.clone(); - let mut expanded: Vec = Vec::new(); + let mut lead = letters.clone(); + let mut prev: Vec = Vec::new(); - loop { - if expansion.len() + expanded.len() >= matches { - break; - } - if expansion.is_empty() { - break; - } + loop { + if lead.len() + prev.len() >= n { + break; + } - let prefix = expansion.pop().expect("Ouch!"); - let sub_expansion: Vec = letters - .iter() - .take(matches - expansion.len() - expanded.len()) - .map(|s| prefix.clone() + s) - .collect(); + if lead.is_empty() { + break; + } + let prefix = lead.pop().unwrap(); - expanded.splice(0..0, sub_expansion); + // generate characters pairs + let gen: Vec = letters + .iter() + .take(n - lead.len() - prev.len()) + .map(|s| prefix.clone() + s) + .collect(); + + // Insert gen in front of prev + prev.splice(0..0, gen); + } + + lead = lead.iter().take(n - prev.len()).cloned().collect(); + lead.append(&mut prev); + lead } - - expansion = expansion.iter().take(matches - expanded.len()).cloned().collect(); - expansion.append(&mut expanded); - expansion - } } -// pub fn get_alphabet(alphabet_name: &str) -> Alphabet { -// let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect(); - -// alphabets -// .get(alphabet_name) -// .expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); - -// Alphabet::new(alphabets[alphabet_name]) -// } - +/// 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 = ALPHABETS.iter().find(|&(name, _letters)| name == &src); - match alphabet { - Some((_name, letters)) => Ok(Alphabet(letters.to_string())), - None => Err(error::ParseError::UnknownAlphabet), - } + 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), + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn simple_matches() { - let alphabet = Alphabet("abcd".to_string()); - let hints = alphabet.hints(3); - assert_eq!(hints, ["a", "b", "c"]); - } + #[test] + fn simple_matches() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(3); + assert_eq!(hints, ["a", "b", "c"]); + } - #[test] - fn composed_matches() { - let alphabet = Alphabet("abcd".to_string()); - let hints = alphabet.hints(6); - assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); - } + #[test] + fn composed_matches() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(6); + assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); + } - #[test] - fn composed_matches_multiple() { - let alphabet = Alphabet("abcd".to_string()); - let hints = alphabet.hints(8); - assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); - } + #[test] + fn composed_matches_multiple() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(8); + assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); + } - #[test] - fn composed_matches_max() { - let alphabet = Alphabet("ab".to_string()); - let hints = alphabet.hints(8); - assert_eq!(hints, ["aa", "ab", "ba", "bb"]); - } + #[test] + fn composed_matches_max_2() { + let alphabet = Alphabet("ab".to_string()); + let hints = alphabet.make_hints(8); + assert_eq!(hints, ["aa", "ab", "ba", "bb"]); + } + + #[test] + fn composed_matches_max_4() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(13); + assert_eq!( + 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 + } } diff --git a/src/lib.rs b/src/lib.rs index 05fd230..f716926 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ pub struct CliOpt { /// 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". - #[clap(short = "k", long, default_value = "qwerty", + #[clap(short = "k", long, default_value = "dvorak", parse(try_from_str = alphabets::parse_alphabet))] alphabet: alphabets::Alphabet, diff --git a/src/state.rs b/src/state.rs index f896186..8a94816 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,8 +1,8 @@ -use super::alphabets::Alphabet; use regex::Regex; use std::collections::HashMap; use std::fmt; +use crate::alphabets::Alphabet; use crate::regexes::{EXCLUDE_PATTERNS, PATTERNS}; #[derive(Clone)] @@ -37,19 +37,19 @@ impl<'a> PartialEq for Match<'a> { pub struct State<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, - regexp: &'a Vec, + custom_regexes: &'a Vec, } impl<'a> State<'a> { pub fn new( lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, - regexp: &'a Vec, + custom_regexes: &'a Vec, ) -> State<'a> { State { lines, alphabet, - regexp, + custom_regexes, } } @@ -62,7 +62,7 @@ impl<'a> State<'a> { .collect::>(); let custom_patterns = self - .regexp + .custom_regexes .iter() .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) .collect::>(); @@ -123,8 +123,7 @@ impl<'a> State<'a> { } } - // let alphabet = super::alphabets::get_alphabet(self.alphabet); - let mut hints = self.alphabet.hints(matches.len()); + let mut hints = self.alphabet.make_hints(matches.len()); // This looks wrong but we do a pop after if !reverse { diff --git a/src/view.rs b/src/view.rs index 577b8af..14fb6ef 100644 --- a/src/view.rs +++ b/src/view.rs @@ -426,6 +426,9 @@ impl<'a> View<'a> { event::Key::Left => self.prev(), event::Key::Right => self.next(), + event::Key::Char(_ch @ 'n') => self.next(), + event::Key::Char(_ch @ 'N') => self.prev(), + // TODO: use a Trie or another data structure to determine // if the entered key belongs to a longer hint. // Attempts at finding a match with a corresponding hint.