///|
/// A named collection of dictionaries for applications with multiple domains.
pub struct DictionaryCatalog {
dictionaries : Map[String, Lexicon]
} derive(Debug)
///|
/// A match annotated with the dictionary that produced it.
pub(all) struct CatalogHit {
source : String
hit : LexiconMatch
} derive(Debug, Eq, ToJson)
///|
/// Summary counters for a dictionary catalog.
pub(all) struct CatalogStats {
dictionary_count : Int
entry_count : Int
node_count : Int
match_count : Int
distinct_sources : Int
} derive(Debug, Eq, ToJson)
///|
pub fn DictionaryCatalog::new() -> DictionaryCatalog {
{ dictionaries: Map([]) }
}
///|
/// Register or replace a named compiled dictionary.
pub fn DictionaryCatalog::register(
self : DictionaryCatalog,
name : String,
lexicon : Lexicon,
) -> Unit {
if name.length() > 0 {
self.dictionaries[name] = lexicon
}
}
///|
/// Compile and register entries under one name.
pub fn DictionaryCatalog::register_entries(
self : DictionaryCatalog,
name : String,
entries : ArrayView[Entry],
) -> Unit {
self.register(name, compile(entries))
}
///|
/// Remove a dictionary and report whether it existed.
pub fn DictionaryCatalog::remove(
self : DictionaryCatalog,
name : String,
) -> Bool {
let existed = self.dictionaries.contains(name)
self.dictionaries.remove(name)
existed
}
///|
/// Return registered names in lexical order.
pub fn DictionaryCatalog::names(self : DictionaryCatalog) -> Array[String] {
let output : Array[String] = []
for name in self.dictionaries.keys() {
output.push(name)
}
output.sort_by((left, right) => left.compare(right))
output
}
///|
pub fn DictionaryCatalog::dictionary_count(self : DictionaryCatalog) -> Int {
self.dictionaries.length()
}
///|
/// Look up an exact keyword in one named dictionary.
pub fn DictionaryCatalog::lookup(
self : DictionaryCatalog,
name : String,
keyword : String,
) -> Array[Entry] {
match self.dictionaries.get(name) {
Some(lexicon) => lexicon.lookup(keyword)
None => []
}
}
///|
/// Return whether any named dictionary contains a keyword.
pub fn DictionaryCatalog::contains(
self : DictionaryCatalog,
keyword : String,
) -> Bool {
for _, lexicon in self.dictionaries {
if lexicon.contains(keyword) {
return true
}
}
false
}
///|
/// Scan all named dictionaries and retain source ownership.
pub fn DictionaryCatalog::scan(
self : DictionaryCatalog,
text : String,
) -> Array[CatalogHit] {
let output : Array[CatalogHit] = []
for name, lexicon in self.dictionaries {
for hit in compile_automaton(lexicon.entries()).find_all(text) {
output.push({ source: name, hit })
}
}
sort_catalog_hits(output)
}
///|
/// Scan the catalog with one shared query policy.
pub fn DictionaryCatalog::scan_query(
self : DictionaryCatalog,
text : String,
query : MatchQuery,
) -> Array[CatalogHit] {
let output : Array[CatalogHit] = []
for name, lexicon in self.dictionaries {
for hit in query_lexicon(lexicon, text, query) {
output.push({ source: name, hit })
}
}
sort_catalog_hits(output)
}
///|
/// Return aggregate catalog counters for a text.
pub fn DictionaryCatalog::stats(
self : DictionaryCatalog,
text : String,
) -> CatalogStats {
let hits = self.scan(text)
let mut entries = 0
let mut nodes = 0
for _, lexicon in self.dictionaries {
entries += lexicon.entry_count()
nodes += lexicon.node_count()
}
let sources : Map[String, Bool] = Map([])
for item in hits {
sources[item.source] = true
}
{
dictionary_count: self.dictionary_count(),
entry_count: entries,
node_count: nodes,
match_count: hits.length(),
distinct_sources: sources.length(),
}
}
///|
/// Return all entries in source-name order.
pub fn DictionaryCatalog::entries(
self : DictionaryCatalog,
) -> Array[(String, Entry)] {
let output : Array[(String, Entry)] = []
for name in self.names() {
match self.dictionaries.get(name) {
Some(lexicon) =>
for item in lexicon.entries() {
output.push((name, item))
}
None => ()
}
}
output
}
///|
/// Merge another catalog into this catalog, replacing equal names.
pub fn DictionaryCatalog::merge(
self : DictionaryCatalog,
other : DictionaryCatalog,
) -> Unit {
for name, lexicon in other.dictionaries {
self.register(name, lexicon)
}
}
///|
/// Export catalog entries with a source column.
pub fn DictionaryCatalog::to_tsv(self : DictionaryCatalog) -> String {
let output = StringBuilder()
output.write_string("source\tkeyword\tlabel\tweight\n")
for pair in self.entries() {
let source = pair.0
let item = pair.1
output.write_string(
"\{escape_dictionary_field(source)}\t\{escape_dictionary_field(item.keyword)}\t\{escape_dictionary_field(item.label)}\t\{item.weight}\n",
)
}
output.to_string()
}
///|
/// Return hits from one source only.
pub fn catalog_hits_for_source(
hits : ArrayView[CatalogHit],
source : String,
) -> Array[CatalogHit] {
[
for item in hits if item.source == source => item
]
}
///|
/// Count hits by source name.
pub fn catalog_count_by_source(
hits : ArrayView[CatalogHit],
) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for item in hits {
counts[item.source] = counts.get_or_default(item.source, 0) + 1
}
counts
}
///|
/// Flatten catalog hits into normal match values.
pub fn catalog_matches(hits : ArrayView[CatalogHit]) -> Array[LexiconMatch] {
[
for item in hits => item.hit
]
}
///|
/// Return a stable catalog hit signature.
pub fn catalog_hit_signature(hit : CatalogHit) -> String {
"\{hit.source}:\{match_signature(hit.hit)}"
}
///|
fn sort_catalog_hits(hits : Array[CatalogHit]) -> Array[CatalogHit] {
hits.sort_by(fn(left, right) {
if left.hit.start != right.hit.start {
left.hit.start - right.hit.start
} else if left.hit.end != right.hit.end {
right.hit.end - right.hit.start - (left.hit.end - left.hit.start)
} else {
left.source.compare(right.source)
}
})
hits
}