use super::term::{Term, TermMetaData};
use super::utils::preprocess;
use scraper::{Html, Node, Selector};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub(crate) struct Document {
terms: HashMap<Term, TermMetaData>,
number_of_terms: usize,
}
impl Document {
pub(super) fn new(text: &str) -> Self {
let mut document = Self::default();
let processed = preprocess(text);
for normalized in processed {
if normalized.len() >= 2 {
document.add_term(&normalized);
document.number_of_terms += 1;
}
}
document
}
fn add_term(&mut self, word: &str) {
let term = Term::new(word);
let metadata = self.terms.entry(term).or_default();
*metadata.count_mut() += 1;
}
pub(crate) fn from_html(raw_html: &str) -> Option<Self> {
let selector = Selector::parse("body").unwrap();
let html = Html::parse_document(raw_html);
let element = html.select(&selector).next()?;
let text = element
.descendants()
.filter_map(|node| {
if !node.value().is_text() && !node.value().is_comment() {
return None;
}
let trimmed = if node.value().is_text() {
node.value().as_text().unwrap().text.trim()
} else {
node.value().as_comment().unwrap().comment.trim()
};
if trimmed.is_empty() {
return None;
}
let parent = node.parent().unwrap().value();
if !parent.is_element() {
return None;
}
if let Node::Element(element) = parent {
if element.name() == "script" || element.name() == "style" {
return None;
}
return Some(format!("{trimmed} "));
}
None
})
.collect::<String>();
Some(Self::new(&text))
}
pub(super) fn term_frequency(&self, term: &Term) -> f32 {
if let Some(metadata) = self.terms.get(term) {
metadata.count() as f32 / self.number_of_terms() as f32
} else {
0.0
}
}
pub(super) fn terms(&self) -> &HashMap<Term, TermMetaData> {
&self.terms
}
fn number_of_terms(&self) -> usize {
self.number_of_terms
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nlp_document_creation_from_text() {
let doc = Document::new("The air quality in Singapore got worse on Wednesday.");
let expected_terms = ["air", "quality", "singapore", "worse", "wednesday"];
for expected in expected_terms {
let term = Term::new(expected);
assert!(doc.terms().contains_key(&term));
assert_eq!(doc.number_of_terms, 5);
assert_eq!(doc.terms().get(&term).unwrap().count(), 1);
let empty: &[f32] = &[];
assert_eq!(doc.terms().get(&term).unwrap().term_frequencies(), empty);
assert_eq!(doc.terms().get(&term).unwrap().document_frequency(), 0);
}
}
#[test]
fn nlp_document_creation_from_html() {
let empty = Document::from_html("<html></html>").unwrap();
assert_eq!(empty.number_of_terms, 0);
let other_empty = Document::from_html("<html><body><p></p></body></html>").unwrap();
assert_eq!(other_empty.number_of_terms, 0);
let third_empty =
Document::from_html("<!DOCTYPE html><html><!DOCTYPE html><p></p></html>").unwrap();
assert_eq!(third_empty.number_of_terms, 0);
let doc = Document::from_html(
"<html><body><p>The air quality in Singapore.</p><!--got worse on Wednesday--></body></html>",
).unwrap();
let expected_terms = ["air", "quality", "singapore", "worse", "wednesday"];
for expected in expected_terms {
let term = Term::new(expected);
assert_eq!(doc.number_of_terms, 5);
assert!(doc.terms().contains_key(&term));
assert_eq!(doc.terms().get(&term).unwrap().count(), 1);
let empty: &[f32] = &[];
assert_eq!(doc.terms().get(&term).unwrap().term_frequencies(), empty);
assert_eq!(doc.terms().get(&term).unwrap().document_frequency(), 0);
}
}
#[test]
fn term_frequency_validation() {
let doc = Document::new("The air quality in Singapore got worse on Wednesday. Air Jordan.");
let air_freq = doc.term_frequency(&Term::new("air"));
let abs_diff = (air_freq - 0.2857143).abs();
assert!(abs_diff <= f32::EPSILON);
let non_existent = doc.term_frequency(&Term::new("derpatronic"));
assert_eq!(non_existent, 0.0);
}
#[test]
fn document_accessor_test() {
let doc = Document::new("The air quality in Singapore got worse on Wednesday.");
let keys = doc.terms().keys().map(|key| key.raw()).collect::<Vec<_>>();
let expected = ["air", "quality", "singapore", "worse", "wednesday"];
assert_eq!(doc.number_of_terms(), 5);
for key in keys {
assert!(expected.contains(&key));
}
}
#[test]
fn document_creation_skips_script_and_style_tags() {
let html = "<body><script>The air quality</script><style>in Singapore</style><p>got worse on Wednesday.</p></body>";
let doc = Document::from_html(html).unwrap();
let keys = doc.terms().keys().map(|key| key.raw()).collect::<Vec<_>>();
let expected = ["worse", "wednesday"];
assert_eq!(doc.number_of_terms(), 2);
for key in keys {
assert!(expected.contains(&key));
}
}
}