use super::transition::{initial_state, transition_state_pooled};
use super::{Algorithm, Intersection, PathNode, State, StatePool, Unrestricted};
use libdictenstein::{CharUnit, DictionaryNode};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
struct SearchEntry<N: DictionaryNode> {
intersection: Box<Intersection<N>>,
g_cost: usize,
f_cost: usize,
}
impl<N: DictionaryNode> SearchEntry<N> {
fn new(intersection: Box<Intersection<N>>, g_cost: usize, h_cost: usize) -> Self {
Self {
intersection,
g_cost,
f_cost: g_cost.saturating_add(h_cost),
}
}
}
impl<N: DictionaryNode> Ord for SearchEntry<N> {
fn cmp(&self, other: &Self) -> Ordering {
match other.f_cost.cmp(&self.f_cost) {
Ordering::Equal => match other.g_cost.cmp(&self.g_cost) {
Ordering::Equal => self.intersection.term().cmp(&other.intersection.term()),
ord => ord,
},
ord => ord,
}
}
}
impl<N: DictionaryNode> PartialOrd for SearchEntry<N> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<N: DictionaryNode> PartialEq for SearchEntry<N> {
fn eq(&self, other: &Self) -> bool {
self.f_cost == other.f_cost
&& self.g_cost == other.g_cost
&& self.intersection.term() == other.intersection.term()
}
}
impl<N: DictionaryNode> Eq for SearchEntry<N> {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PriorityCandidate {
pub term: String,
pub distance: usize,
}
pub struct PriorityQueryIterator<N: DictionaryNode> {
queue: BinaryHeap<SearchEntry<N>>,
query: Vec<N::Unit>,
query_len: usize,
max_distance: usize,
algorithm: Algorithm,
state_pool: StatePool,
}
impl<N: DictionaryNode> PriorityQueryIterator<N> {
pub fn new(root: N, query: &str, max_distance: usize, algorithm: Algorithm) -> Self {
let query_units = N::Unit::from_str(query);
let query_len = query_units.len();
let initial = initial_state(query_len, max_distance, algorithm);
let mut queue = BinaryHeap::with_capacity(64);
let root_intersection = Box::new(Intersection::new(root, initial));
let g_cost = 0; let h_cost = query_len;
queue.push(SearchEntry::new(root_intersection, g_cost, h_cost));
Self {
queue,
query: query_units,
query_len,
max_distance,
algorithm,
state_pool: StatePool::new(),
}
}
#[inline]
fn heuristic(&self, state: &State) -> usize {
let max_consumed = state
.positions()
.iter()
.map(|p| p.term_index)
.max()
.unwrap_or(0);
self.query_len.saturating_sub(max_consumed)
}
fn advance(&mut self) -> Option<PriorityCandidate> {
while let Some(entry) = self.queue.pop() {
if entry.intersection.is_final() {
let distance = entry
.intersection
.state
.infer_distance(self.query_len)
.unwrap_or(usize::MAX);
if distance <= self.max_distance {
self.expand_children(&entry);
return Some(PriorityCandidate {
term: entry.intersection.term(),
distance,
});
}
}
self.expand_children(&entry);
}
None
}
#[inline]
fn expand_children(&mut self, entry: &SearchEntry<N>) {
for (label, child_node) in entry.intersection.node.edges() {
if let Some(next_state) = transition_state_pooled(
&entry.intersection.state,
&mut self.state_pool,
Unrestricted, label,
&self.query,
self.max_distance,
self.algorithm,
false, ) {
let g_cost = next_state.min_distance().unwrap_or(0);
if g_cost > self.max_distance {
continue;
}
let h_cost = self.heuristic(&next_state);
let parent_path = entry.intersection.label.map(|current_label| {
Box::new(PathNode::new(
current_label,
entry.intersection.parent.clone(),
))
});
let child_intersection = Box::new(Intersection::with_parent(
label,
child_node,
next_state,
parent_path,
));
self.queue
.push(SearchEntry::new(child_intersection, g_cost, h_cost));
}
}
}
}
impl<N: DictionaryNode> Iterator for PriorityQueryIterator<N> {
type Item = PriorityCandidate;
fn next(&mut self) -> Option<Self::Item> {
self.advance()
}
}
pub fn priority_query<N: DictionaryNode>(
root: N,
query: &str,
max_distance: usize,
algorithm: Algorithm,
) -> PriorityQueryIterator<N> {
PriorityQueryIterator::new(root, query, max_distance, algorithm)
}
#[cfg(test)]
mod tests {
use super::*;
use libdictenstein::Dictionary;
fn test_dict() -> libdictenstein::dynamic_dawg::DynamicDawg {
let dawg = libdictenstein::dynamic_dawg::DynamicDawg::new();
for term in ["apple", "apply", "appeal", "banana", "test", "best", "rest"] {
dawg.insert(term);
}
dawg
}
#[test]
fn test_exact_match() {
let dict = test_dict();
let mut iter = PriorityQueryIterator::new(dict.root(), "apple", 2, Algorithm::Standard);
let first = iter.next();
assert!(first.is_some());
let candidate = first.expect("test fixture: first candidate exists (asserted above)");
assert_eq!(candidate.term, "apple");
assert_eq!(candidate.distance, 0);
}
#[test]
fn test_close_matches() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "aple", 2, Algorithm::Standard);
let results: Vec<_> = iter.collect();
let terms: Vec<_> = results.iter().map(|c| c.term.as_str()).collect();
assert!(
terms.contains(&"apple"),
"Should contain 'apple': {:?}",
terms
);
}
#[test]
fn test_distance_ordering() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "test", 2, Algorithm::Standard);
let results: Vec<_> = iter.collect();
if results.len() >= 2 {
let first_dist = results[0].distance;
assert!(
first_dist <= 1,
"First result distance should be <= 1, got {}",
first_dist
);
}
}
#[test]
fn test_max_distance_respected() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "xyz", 1, Algorithm::Standard);
let results: Vec<_> = iter.collect();
for candidate in &results {
assert!(
candidate.distance <= 1,
"Distance {} exceeds max 1 for term {}",
candidate.distance,
candidate.term
);
}
}
#[test]
fn test_empty_query() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "", 3, Algorithm::Standard);
let results: Vec<_> = iter.collect();
let terms: Vec<_> = results.iter().map(|c| c.term.as_str()).collect();
let _ = terms;
}
#[test]
fn test_transposition() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "tset", 2, Algorithm::Transposition);
let results: Vec<_> = iter.collect();
let test_result = results.iter().find(|c| c.term == "test");
assert!(
test_result.is_some(),
"Should find 'test' for 'tset' with transposition"
);
if let Some(candidate) = test_result {
assert_eq!(candidate.distance, 1, "Transposition should be distance 1");
}
}
#[test]
fn test_take_early() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "test", 2, Algorithm::Standard);
let results: Vec<_> = iter.take(2).collect();
assert!(results.len() <= 2);
}
#[test]
fn test_no_matches() {
let dict = test_dict();
let iter = PriorityQueryIterator::new(dict.root(), "zzzzzzzzz", 1, Algorithm::Standard);
let results: Vec<_> = iter.collect();
assert!(
results.is_empty(),
"Should find no matches for 'zzzzzzzzz' within distance 1"
);
}
}