use crate::matcher::Match;
use crate::prelude::*;
pub(crate) const fn to_lower(ch: u8) -> u8 {
if b'A' <= ch && ch <= b'Z' { ch + b'a' - b'A' } else { ch }
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default, Hash)]
enum Class {
#[default]
Exact,
Misplaced,
Wrong,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default, Hash)]
pub(crate) struct LetterInfo {
ch: u8,
class: Class,
}
#[must_use]
pub(crate) fn matches(word: &[u8], guess: &[LetterInfo]) -> bool {
debug_assert!(word.len() == guess.len());
let mut used = vec![false; word.len()];
for i in 0..guess.len() {
if guess[i].class == Class::Exact {
if word[i] != guess[i].ch {
return false;
}
used[i] = true;
}
}
for (i, g) in guess.iter().enumerate() {
if g.class == Class::Misplaced {
for j in 0..word.len() {
if i == j && word[j] == g.ch {
return false;
}
if g.ch == word[j] && !used[j] {
used[j] = true;
break;
}
if j == word.len() - 1 {
return false;
}
}
}
}
for g in guess {
if g.class == Class::Wrong {
for j in 0..word.len() {
if g.ch == word[j] && !used[j] {
return false;
}
}
}
}
true
}
#[must_use]
fn multi_match(word: &[u8], guess: &[Vec<LetterInfo>]) -> bool {
for g in guess {
if word.len() != g.len() {
return false;
}
if !matches(word, g) {
return false;
}
}
true
}
#[derive(Debug, Clone)]
pub struct SolverMatch {
guesses: Vec<Vec<LetterInfo>>,
}
fn make_letter_info(data: &[u8]) -> Result<Vec<LetterInfo>> {
let mut ret = Vec::with_capacity(data.len());
let mut was_plus = false;
for &ch in data {
if ch == b'+' {
if was_plus {
return err!("Consecutive '+' characters in solver pattern");
}
was_plus = true;
} else if ch.is_ascii_lowercase() {
if was_plus {
ret.push(LetterInfo { ch, class: Class::Exact });
was_plus = false;
} else {
ret.push(LetterInfo { ch, class: Class::Wrong });
}
} else if ch.is_ascii_uppercase() {
ret.push(LetterInfo { ch: to_lower(ch), class: Class::Misplaced });
} else {
return err!("Invalid character in solver pattern: {}", ch as char);
}
}
Ok(ret)
}
impl SolverMatch {
pub fn new(data: &str) -> Result<Self> {
let words: Vec<&str> = data.split(',').collect();
if words.is_empty() {
return err!("Solver matcher requires at least exact pattern");
}
if words[0].is_empty() {
return err!("Solver patterns must not be empty.");
}
let mut guesses = Vec::new();
let mut len = 0usize;
for word in &words {
let guess = make_letter_info(word.as_bytes())?;
if len == 0 {
len = guess.len();
} else if guess.len() != len {
return err!("All patterns in solver matcher must have the same length");
}
guesses.push(guess);
}
Ok(Self { guesses })
}
}
impl Match for SolverMatch {
fn smatch(&self, buff: &str) -> bool {
multi_match(buff.as_bytes(), &self.guesses)
}
fn umatch(&self, buff: &[u8]) -> bool {
multi_match(buff, &self.guesses)
}
fn show(&self) -> String {
"Solver Match".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_match(word: &[u8], guess: &str) -> bool {
let sm = SolverMatch::new(guess).unwrap();
sm.umatch(word)
}
#[test]
fn solve() {
assert!(test_match(b"apple", "PxyzP"));
assert!(test_match(b"apple", "qrtPA"));
assert!(test_match(b"apple", "+a+p+p+l+e,vwxyz"));
assert!(test_match(b"apple", "xyzPA"));
assert!(!test_match(b"apple", "xyzLA"));
assert!(!test_match(b"apple", "xyzAA"));
}
#[test]
fn multi_solve() {
assert!(test_match(b"apple", "PxyzP"));
assert!(test_match(b"apple", "xyzPA"));
assert!(!test_match(b"apple", "xyzLA"));
assert!(!test_match(b"apple", "xyzAA"));
assert!(test_match(b"apple", "uvxyz"));
assert!(test_match(b"apple", "qrtPA"));
assert!(test_match(b"apple", "uvxyz,qrtPA"));
assert!(test_match(b"weigh", "starE,no+ily"));
assert!(test_match(b"weigh", "starE,no+ily,b+e+i+ge"));
}
}