use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use crate::user_dict::{UserDictionary, UserEntry};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct DomainId(pub String);
impl std::fmt::Display for DomainId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub struct DomainDictionary {
pub domain: DomainId,
pub priority: u8,
pub dictionary: Arc<UserDictionary>,
pub source_path: Option<PathBuf>,
pub loaded_at: SystemTime,
}
impl std::fmt::Debug for DomainDictionary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DomainDictionary")
.field("domain", &self.domain)
.field("priority", &self.priority)
.field("entry_count", &self.dictionary.len())
.field("source_path", &self.source_path)
.field("loaded_at", &self.loaded_at)
.finish()
}
}
impl DomainDictionary {
fn new(
domain: DomainId,
priority: u8,
dictionary: Arc<UserDictionary>,
source_path: Option<PathBuf>,
) -> Self {
Self {
domain,
priority,
dictionary,
source_path,
loaded_at: SystemTime::now(),
}
}
}
#[derive(Debug, Default)]
pub struct DomainStack {
domains: Vec<DomainDictionary>,
}
impl DomainStack {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_domain(
&mut self,
domain: DomainId,
priority: u8,
dict: Arc<UserDictionary>,
source: Option<PathBuf>,
) {
self.domains.retain(|d| d.domain != domain);
let entry = DomainDictionary::new(domain, priority, dict, source);
self.domains.push(entry);
self.domains.sort_by_key(|d| d.priority);
}
pub fn remove_domain(&mut self, domain: &DomainId) -> Option<DomainDictionary> {
if let Some(pos) = self.domains.iter().position(|d| &d.domain == domain) {
Some(self.domains.remove(pos))
} else {
None
}
}
#[must_use]
pub fn get_domain(&self, domain: &DomainId) -> Option<&DomainDictionary> {
self.domains.iter().find(|d| &d.domain == domain)
}
#[must_use]
pub fn list_domains(&self) -> Vec<(DomainId, u8, usize)> {
self.domains
.iter()
.map(|d| (d.domain.clone(), d.priority, d.dictionary.len()))
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.domains.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.domains.is_empty()
}
#[must_use]
pub fn common_prefix_search<'a>(&'a self, text: &str) -> Vec<&'a UserEntry> {
self.domains
.iter()
.flat_map(|d| d.dictionary.common_prefix_search(text))
.collect()
}
#[must_use]
pub fn lookup<'a>(&'a self, surface: &str) -> Vec<&'a UserEntry> {
self.domains
.iter()
.flat_map(|d| d.dictionary.lookup(surface))
.collect()
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
fn make_dict(entries: &[(&str, &str, i16)]) -> Arc<UserDictionary> {
let mut d = UserDictionary::new();
for &(surface, pos, cost) in entries {
d.add_entry(surface, pos, Some(cost), None);
}
Arc::new(d)
}
#[test]
fn test_empty_stack() {
let stack = DomainStack::new();
assert!(stack.is_empty());
assert_eq!(stack.len(), 0);
assert!(stack.list_domains().is_empty());
assert!(stack.lookup("anything").is_empty());
assert!(stack.common_prefix_search("anything").is_empty());
}
#[test]
fn test_add_two_domains_priority_ordering() {
let mut stack = DomainStack::new();
let low = make_dict(&[("하위", "NNG", -100)]);
let high = make_dict(&[("상위", "NNP", -1000)]);
stack.add_domain(DomainId("low".into()), 10, low, None);
stack.add_domain(DomainId("high".into()), 0, high, None);
let listing = stack.list_domains();
assert_eq!(listing.len(), 2);
assert_eq!(listing[0].0, DomainId("high".into()));
assert_eq!(listing[0].1, 0);
assert_eq!(listing[1].0, DomainId("low".into()));
assert_eq!(listing[1].1, 10);
}
#[test]
fn test_common_prefix_search_returns_entries_from_all_domains() {
let mut stack = DomainStack::new();
let d1 = make_dict(&[("형태", "NNG", -100), ("형태소", "NNG", -200)]);
let d2 = make_dict(&[("형태소분석", "NNG", -300)]);
stack.add_domain(DomainId("d1".into()), 0, d1, None);
stack.add_domain(DomainId("d2".into()), 1, d2, None);
let results = stack.common_prefix_search("형태소분석기");
assert_eq!(results.len(), 3);
assert_eq!(results[0].surface, "형태");
assert_eq!(results[1].surface, "형태소");
assert_eq!(results[2].surface, "형태소분석");
}
#[test]
fn test_remove_domain_returns_correct_domain() {
let mut stack = DomainStack::new();
let d1 = make_dict(&[("단어1", "NNG", 0)]);
let d2 = make_dict(&[("단어2", "NNG", 0)]);
stack.add_domain(DomainId("alpha".into()), 0, d1, None);
stack.add_domain(DomainId("beta".into()), 1, d2, None);
assert_eq!(stack.len(), 2);
let removed = stack.remove_domain(&DomainId("alpha".into()));
assert!(removed.is_some());
assert_eq!(removed.unwrap().domain, DomainId("alpha".into()));
assert_eq!(stack.len(), 1);
let none = stack.remove_domain(&DomainId("alpha".into()));
assert!(none.is_none());
}
#[test]
fn test_list_domains_returns_all_ids_with_entry_counts() {
let mut stack = DomainStack::new();
stack.add_domain(
DomainId("a".into()),
2,
make_dict(&[("x", "NNG", 0), ("y", "NNG", 0)]),
None,
);
stack.add_domain(DomainId("b".into()), 1, make_dict(&[("z", "NNG", 0)]), None);
let listing = stack.list_domains();
assert_eq!(listing[0].0, DomainId("b".into()));
assert_eq!(listing[0].2, 1); assert_eq!(listing[1].0, DomainId("a".into()));
assert_eq!(listing[1].2, 2); }
#[test]
fn test_duplicate_domain_add_replaces_existing() {
let mut stack = DomainStack::new();
let v1 = make_dict(&[("old_entry", "NNG", 0)]);
let v2 = make_dict(&[("new_entry", "NNP", -500)]);
stack.add_domain(DomainId("same".into()), 0, v1, None);
assert_eq!(stack.len(), 1);
assert!(!stack.lookup("old_entry").is_empty());
stack.add_domain(DomainId("same".into()), 0, v2, None);
assert_eq!(stack.len(), 1);
assert!(stack.lookup("old_entry").is_empty());
assert!(!stack.lookup("new_entry").is_empty());
}
#[test]
fn test_lookup_returns_entries_in_priority_order() {
let mut stack = DomainStack::new();
let high = make_dict(&[("공통", "NNP", -2000)]);
let low = make_dict(&[("공통", "NNG", -100)]);
stack.add_domain(DomainId("high".into()), 0, high, None);
stack.add_domain(DomainId("low".into()), 5, low, None);
let results = stack.lookup("공통");
assert_eq!(results.len(), 2);
assert_eq!(results[0].pos, "NNP");
assert_eq!(results[1].pos, "NNG");
}
}