///|
/// Errors raised while parsing a UTF-8 Chinese dictionary text resource.
///
/// Line numbers are one-based. The accepted format is
/// `word frequency [tag]`; an optional tag is parsed but not retained.
pub(all) suberror ChineseDictionaryResourceError {
InvalidFieldCount(Int, Int)
InvalidFrequency(Int, String)
InvalidWord(Int, String)
DuplicateWord(Int, String)
FrequencyOverflow(Int)
LexiconError(ChineseLexiconError)
} derive(Eq, @debug.Debug)
///|
pub impl Show for ChineseDictionaryResourceError with fn output(self, logger) {
match self {
InvalidFieldCount(line, count) =>
logger.write_string(
"Chinese dictionary line \{line} must contain 2 or 3 fields, found \{count}",
)
InvalidFrequency(line, value) =>
logger.write_string(
"Chinese dictionary line \{line} has invalid positive frequency: \{value}",
)
InvalidWord(line, word) =>
logger.write_string(
"Chinese dictionary line \{line} has a non-Han word: \{word}",
)
DuplicateWord(line, word) =>
logger.write_string(
"Chinese dictionary line \{line} repeats word: \{word}",
)
FrequencyOverflow(line) =>
logger.write_string(
"Chinese dictionary total frequency overflows at line \{line}",
)
LexiconError(error) =>
logger.write_string(
"Chinese dictionary passed parsing but failed lexicon construction: \{error}",
)
}
}
///|
/// Splits one dictionary line on Unicode whitespace.
///
/// A UTF-8 BOM is accepted only before the first field on line one. Blank
/// lines and lines whose first non-whitespace character is `#` return no
/// fields.
fn dictionary_line_fields(line : String, line_number : Int) -> Array[String] {
let characters = line.to_array()
let fields : Array[String] = []
let mut cursor = 0
if line_number == 1 &&
characters.length() > 0 &&
characters[0].to_int() == 0xFEFF {
cursor = 1
}
while cursor < characters.length() && characters[cursor].is_whitespace() {
cursor += 1
}
if cursor >= characters.length() || characters[cursor] == '#' {
return fields
}
while cursor < characters.length() {
while cursor < characters.length() && characters[cursor].is_whitespace() {
cursor += 1
}
if cursor >= characters.length() {
break
}
let start = cursor
while cursor < characters.length() && !characters[cursor].is_whitespace() {
cursor += 1
}
fields.push(String::from_array(characters[start:cursor]))
}
fields
}
///|
/// Parses one strictly positive base-10 Int without accepting signs or
/// overflowing MoonBit's 32-bit Int.
fn parse_dictionary_frequency(value : String) -> Int? {
let characters = value.to_array()
if characters.length() == 0 {
return None
}
let mut result = 0
for character in characters {
let code = character.to_int()
if code < 0x30 || code > 0x39 {
return None
}
let digit = code - 0x30
if result > (0x7FFFFFFF - digit) / 10 {
return None
}
result = result * 10 + digit
}
if result > 0 {
Some(result)
} else {
None
}
}
///|
/// Parses a caller-provided UTF-8 dictionary resource into an immutable Trie.
///
/// File-system access and caching stay outside the analysis package so the
/// same parser works for native, JavaScript, WASM, and custom Directory data.
pub fn ChineseLexicon::from_dictionary_text(
text : String,
) -> ChineseLexicon raise ChineseDictionaryResourceError {
let builder = ChineseLexiconBuilder::new()
let mut line_number = 0
for line in text.split("\n") {
line_number += 1
let fields = dictionary_line_fields(line.to_owned(), line_number)
if fields.length() == 0 {
continue
}
guard fields.length() == 2 || fields.length() == 3 else {
raise ChineseDictionaryResourceError::InvalidFieldCount(
line_number,
fields.length(),
)
}
let word = fields[0]
for character in word {
guard is_han_character(character) else {
raise ChineseDictionaryResourceError::InvalidWord(line_number, word)
}
}
let frequency = match parse_dictionary_frequency(fields[1]) {
Some(value) => value
None =>
raise ChineseDictionaryResourceError::InvalidFrequency(
line_number,
fields[1],
)
}
builder.add(word, frequency, true) catch {
ChineseLexiconError::DuplicateWord(_) =>
raise ChineseDictionaryResourceError::DuplicateWord(line_number, word)
ChineseLexiconError::FrequencyOverflow =>
raise ChineseDictionaryResourceError::FrequencyOverflow(line_number)
error => raise ChineseDictionaryResourceError::LexiconError(error)
}
}
builder.finish()
}