snomed-ecl-engine 0.1.1

Compact SNOMED CT ECL engine and RF2 index builder
Documentation
use super::{Search, TextError};
use crate::ecl::{SearchTerm, MAX_NODES, MAX_QUERY_BYTES};
use std::collections::BTreeMap;

pub struct Terms<'a> {
    source: &'a [SearchTerm],
    languages: BTreeMap<[u8; 2], Vec<Vec<Option<Search>>>>,
    buffer: Vec<u16>,
}

impl<'a> Terms<'a> {
    pub fn new(source: &'a [SearchTerm]) -> Result<Self, TextError> {
        let mut nodes = 0usize;
        let mut bytes = 0usize;
        for term in source {
            nodes = nodes.saturating_add(1);
            match term {
                SearchTerm::Match(words) => {
                    nodes = nodes.saturating_add(words.len());
                    for word in words {
                        bytes = bytes.saturating_add(word.len());
                    }
                }
                SearchTerm::Wild(parts) => {
                    nodes = nodes.saturating_add(parts.len());
                    for text in parts.iter().flatten() {
                        bytes = bytes.saturating_add(text.len());
                    }
                    if parts.windows(2).any(|w| w[0].is_some() && w[1].is_some()) {
                        return Err(TextError::InvalidInput);
                    }
                }
            }
        }
        if source.is_empty()
            || nodes > MAX_NODES
            || bytes > MAX_QUERY_BYTES
            || source.iter().any(|term| match term {
                SearchTerm::Match(words) => words.is_empty() || words.iter().any(String::is_empty),
                SearchTerm::Wild(parts) => {
                    parts.is_empty() || parts.iter().flatten().any(String::is_empty)
                }
            })
        {
            return Err(TextError::InvalidInput);
        }
        Ok(Self {
            source,
            languages: BTreeMap::new(),
            buffer: Vec::new(),
        })
    }

    pub fn work(&self, target: &str) -> usize {
        self.work_bytes(target.len())
    }

    pub fn work_bytes(&self, target_bytes: usize) -> usize {
        self.source
            .iter()
            .map(|term| match term {
                SearchTerm::Match(words) => words
                    .iter()
                    .map(|p| p.len().saturating_add(target_bytes).saturating_add(1))
                    .sum::<usize>(),
                SearchTerm::Wild(parts) => parts
                    .iter()
                    .flatten()
                    .map(|p| p.len().saturating_add(target_bytes).saturating_add(1))
                    .sum::<usize>()
                    .saturating_add(1),
            })
            .fold(0, usize::saturating_add)
    }

    pub fn matches(&mut self, target: &str, language: [u8; 2]) -> Result<bool, TextError> {
        if !self.languages.contains_key(&language) {
            let mut compiled = Vec::with_capacity(self.source.len());
            for term in self.source {
                compiled.push(match term {
                    SearchTerm::Match(words) => words.iter().map(|_| None).collect(),
                    SearchTerm::Wild(parts) => parts.iter().flatten().map(|_| None).collect(),
                });
            }
            self.languages.insert(language, compiled);
        }
        self.buffer.clear();
        self.buffer.extend(target.encode_utf16());
        let compiled = self.languages.get_mut(&language).unwrap();
        for (term, searches) in self.source.iter().zip(compiled) {
            let matched = match term {
                SearchTerm::Match(words) => {
                    let mut matched = true;
                    for (word, search) in words.iter().zip(searches) {
                        let search = compile(search, word, language)?;
                        if search.find(&self.buffer, 0, 4)?.is_none() {
                            matched = false;
                            break;
                        }
                    }
                    matched
                }
                SearchTerm::Wild(parts) => {
                    let mut end = 0;
                    let mut matched = true;
                    let mut searches = searches.iter_mut();
                    for (i, part) in parts.iter().enumerate() {
                        let Some(text) = part else {
                            continue;
                        };
                        let flags = u8::from(i == 0) | (u8::from(i + 1 == parts.len()) << 1);
                        let search = compile(searches.next().unwrap(), text, language)?;
                        let Some(found) = search.find(&self.buffer, end, flags)? else {
                            matched = false;
                            break;
                        };
                        end = found.end;
                    }
                    matched
                }
            };
            if matched {
                return Ok(true);
            }
        }
        Ok(false)
    }
}

fn compile<'a>(
    slot: &'a mut Option<Search>,
    pattern: &str,
    language: [u8; 2],
) -> Result<&'a mut Search, TextError> {
    if slot.is_none() {
        *slot = Some(Search::new(pattern, language)?);
    }
    Ok(slot.as_mut().unwrap())
}