refactor: wip alphabet & regexes

This commit is contained in:
graelo 2020-05-30 19:28:54 +02:00
parent 836d5bdc97
commit 53f7084303
4 changed files with 124 additions and 97 deletions

View file

@ -1,116 +1,141 @@
use crate::error; use crate::error;
const ALPHABETS: [(&'static str, &'static str); 22] = [ const ALPHABETS: [(&'static str, &'static str); 22] = [
("numeric", "1234567890"), ("numeric", "1234567890"),
("abcd", "abcd"), ("abcd", "abcd"),
("qwerty", "asdfqwerzxcvjklmiuopghtybn"), ("qwerty", "asdfqwerzxcvjklmiuopghtybn"),
("qwerty-homerow", "asdfjklgh"), ("qwerty-homerow", "asdfjklgh"),
("qwerty-left-hand", "asdfqwerzcxv"), ("qwerty-left-hand", "asdfqwerzcxv"),
("qwerty-right-hand", "jkluiopmyhn"), ("qwerty-right-hand", "jkluiopmyhn"),
("azerty", "qsdfazerwxcvjklmuiopghtybn"), ("azerty", "qsdfazerwxcvjklmuiopghtybn"),
("azerty-homerow", "qsdfjkmgh"), ("azerty-homerow", "qsdfjkmgh"),
("azerty-left-hand", "qsdfazerwxcv"), ("azerty-left-hand", "qsdfazerwxcv"),
("azerty-right-hand", "jklmuiophyn"), ("azerty-right-hand", "jklmuiophyn"),
("qwertz", "asdfqweryxcvjkluiopmghtzbn"), ("qwertz", "asdfqweryxcvjkluiopmghtzbn"),
("qwertz-homerow", "asdfghjkl"), ("qwertz-homerow", "asdfghjkl"),
("qwertz-left-hand", "asdfqweryxcv"), ("qwertz-left-hand", "asdfqweryxcv"),
("qwertz-right-hand", "jkluiopmhzn"), ("qwertz-right-hand", "jkluiopmhzn"),
("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"), ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"),
("dvorak-homerow", "aoeuhtnsid"), ("dvorak-homerow", "aoeuhtnsid"),
("dvorak-left-hand", "aoeupqjkyix"), ("dvorak-left-hand", "aoeupqjkyix"),
("dvorak-right-hand", "htnsgcrlmwvz"), ("dvorak-right-hand", "htnsgcrlmwvz"),
("colemak", "arstqwfpzxcvneioluymdhgjbk"), ("colemak", "arstqwfpzxcvneioluymdhgjbk"),
("colemak-homerow", "arstneiodh"), ("colemak-homerow", "arstneiodh"),
("colemak-left-hand", "arstqwfpzxcv"), ("colemak-left-hand", "arstqwfpzxcv"),
("colemak-right-hand", "neioluymjhk"), ("colemak-right-hand", "neioluymjhk"),
]; ];
// pub struct Alphabet<'a> {
// letters: &'a str,
// }
/// Type-safe string alphabet (newtype). /// Type-safe string alphabet (newtype).
#[derive(Debug)] #[derive(Debug)]
pub struct Alphabet(pub String); pub struct Alphabet(pub String);
impl Alphabet { impl Alphabet {
pub fn hints(&self, matches: usize) -> Vec<String> { /// Create `n` hints.
let letters: Vec<String> = self.0.chars().map(|s| s.to_string()).collect(); ///
/// ```
/// // 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<String> {
let letters: Vec<String> = self.0.chars().map(|s| s.to_string()).collect();
let mut expansion = letters.clone(); let mut lead = letters.clone();
let mut expanded: Vec<String> = Vec::new(); let mut prev: Vec<String> = Vec::new();
loop { loop {
if expansion.len() + expanded.len() >= matches { if lead.len() + prev.len() >= n {
break; break;
} }
if expansion.is_empty() {
break;
}
let prefix = expansion.pop().expect("Ouch!"); if lead.is_empty() {
let sub_expansion: Vec<String> = letters break;
.iter() }
.take(matches - expansion.len() - expanded.len()) let prefix = lead.pop().unwrap();
.map(|s| prefix.clone() + s)
.collect();
expanded.splice(0..0, sub_expansion); // generate characters pairs
let gen: Vec<String> = 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 { /// Parse a name string into `Alphabet`, supporting the CLI.
// let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect(); ///
/// # Note
// alphabets ///
// .get(alphabet_name) /// Letters 'n' and 'N' are systematically removed to prevent conflict with
// .expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); /// navigation keys (arrows and 'n' 'N').
// Alphabet::new(alphabets[alphabet_name])
// }
pub fn parse_alphabet(src: &str) -> Result<Alphabet, error::ParseError> { pub fn parse_alphabet(src: &str) -> Result<Alphabet, error::ParseError> {
let alphabet = ALPHABETS.iter().find(|&(name, _letters)| name == &src); let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src);
match alphabet {
Some((_name, letters)) => Ok(Alphabet(letters.to_string())), match alphabet_pair {
None => Err(error::ParseError::UnknownAlphabet), Some((_name, letters)) => {
} let letters = letters.replace(&['n', 'N'][..], "");
Ok(Alphabet(letters.to_string()))
}
None => Err(error::ParseError::UnknownAlphabet),
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn simple_matches() { fn simple_matches() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.hints(3); let hints = alphabet.make_hints(3);
assert_eq!(hints, ["a", "b", "c"]); assert_eq!(hints, ["a", "b", "c"]);
} }
#[test] #[test]
fn composed_matches() { fn composed_matches() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.hints(6); let hints = alphabet.make_hints(6);
assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]);
} }
#[test] #[test]
fn composed_matches_multiple() { fn composed_matches_multiple() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.hints(8); let hints = alphabet.make_hints(8);
assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]);
} }
#[test] #[test]
fn composed_matches_max() { fn composed_matches_max_2() {
let alphabet = Alphabet("ab".to_string()); let alphabet = Alphabet("ab".to_string());
let hints = alphabet.hints(8); let hints = alphabet.make_hints(8);
assert_eq!(hints, ["aa", "ab", "ba", "bb"]); 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
}
} }

View file

@ -59,7 +59,7 @@ pub struct CliOpt {
/// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand",
/// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz",
/// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". /// "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))] parse(try_from_str = alphabets::parse_alphabet))]
alphabet: alphabets::Alphabet, alphabet: alphabets::Alphabet,

View file

@ -1,8 +1,8 @@
use super::alphabets::Alphabet;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use crate::alphabets::Alphabet;
use crate::regexes::{EXCLUDE_PATTERNS, PATTERNS}; use crate::regexes::{EXCLUDE_PATTERNS, PATTERNS};
#[derive(Clone)] #[derive(Clone)]
@ -37,19 +37,19 @@ impl<'a> PartialEq for Match<'a> {
pub struct State<'a> { pub struct State<'a> {
pub lines: &'a Vec<&'a str>, pub lines: &'a Vec<&'a str>,
alphabet: &'a Alphabet, alphabet: &'a Alphabet,
regexp: &'a Vec<String>, custom_regexes: &'a Vec<String>,
} }
impl<'a> State<'a> { impl<'a> State<'a> {
pub fn new( pub fn new(
lines: &'a Vec<&'a str>, lines: &'a Vec<&'a str>,
alphabet: &'a Alphabet, alphabet: &'a Alphabet,
regexp: &'a Vec<String>, custom_regexes: &'a Vec<String>,
) -> State<'a> { ) -> State<'a> {
State { State {
lines, lines,
alphabet, alphabet,
regexp, custom_regexes,
} }
} }
@ -62,7 +62,7 @@ impl<'a> State<'a> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let custom_patterns = self let custom_patterns = self
.regexp .custom_regexes
.iter() .iter()
.map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp")))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -123,8 +123,7 @@ impl<'a> State<'a> {
} }
} }
// let alphabet = super::alphabets::get_alphabet(self.alphabet); let mut hints = self.alphabet.make_hints(matches.len());
let mut hints = self.alphabet.hints(matches.len());
// This looks wrong but we do a pop after // This looks wrong but we do a pop after
if !reverse { if !reverse {

View file

@ -426,6 +426,9 @@ impl<'a> View<'a> {
event::Key::Left => self.prev(), event::Key::Left => self.prev(),
event::Key::Right => self.next(), 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 // TODO: use a Trie or another data structure to determine
// if the entered key belongs to a longer hint. // if the entered key belongs to a longer hint.
// Attempts at finding a match with a corresponding hint. // Attempts at finding a match with a corresponding hint.