///|
fn is_ascii_word_letter(character : Char) -> Bool {
(character >= 'A' && character <= 'Z') ||
(character >= 'a' && character <= 'z')
}
///|
fn is_scan_candidate_char(
character : Char,
mode : RomanMode,
accept_unicode : Bool,
) -> Bool {
if is_ascii_roman_letter(character) {
return true
}
if mode == ParenthesizedThousands && (character == '(' || character == ')') {
return true
}
accept_unicode && unicode_roman_expansion(character) is Some(_)
}
///|
fn scan_substring(chars : Array[Char], start : Int, end : Int) -> String {
let selected : Array[Char] = []
for index = start; index < end; index = index + 1 {
selected.push(chars[index])
}
String::from_array(selected)
}
///|
fn has_scan_word_boundaries(
chars : Array[Char],
start : Int,
end : Int,
) -> Bool {
let left_is_word = start > 0 && is_ascii_word_letter(chars[start - 1])
let right_is_word = end < chars.length() && is_ascii_word_letter(chars[end])
!left_is_word && !right_is_word
}
///|
fn retain_scan_rejection(
rejections : Array[RomanScanRejection],
config : ScanConfig,
span : SourceSpan,
source_text : String,
reason : ScanRejectionReason,
) -> Unit {
if config.retain_rejected {
rejections.push({ span, source_text, reason })
}
}
///|
/// Scan complete lexical candidates and parse each with the selected profile.
pub fn scan_roman_text(
text : String,
config : ScanConfig,
) -> Result[RomanScanReport, ConfigError] {
match validate_scan_config(config) {
Err(error) => return Err(error)
Ok(_) => ()
}
let chars = text.to_array()
let matches : Array[RomanScanMatch] = []
let rejections : Array[RomanScanRejection] = []
let mut candidates_examined = 0
let mut index = 0
while index < chars.length() {
if !is_scan_candidate_char(
chars[index],
config.parse_config.mode,
config.parse_config.accept_unicode,
) {
index = index + 1
continue
}
let start = index
while index < chars.length() &&
is_scan_candidate_char(
chars[index],
config.parse_config.mode,
config.parse_config.accept_unicode,
) {
index = index + 1
}
let end = index
if !has_scan_word_boundaries(chars, start, end) {
continue
}
candidates_examined = candidates_examined + 1
let span : SourceSpan = { start, end }
let source_text = scan_substring(chars, start, end)
let source_length = end - start
if source_length > config.max_candidate_length {
retain_scan_rejection(
rejections,
config,
span,
source_text,
CandidateTooLong(source_length),
)
continue
}
match parse_with_config(source_text, config.parse_config) {
Ok(report) =>
if report.normalized.to_array().length() == 1 &&
!config.include_single_symbol {
retain_scan_rejection(
rejections,
config,
span,
source_text,
SingleSymbolExcluded,
)
} else {
matches.push({ span, source_text, report })
}
Err(error) =>
retain_scan_rejection(
rejections,
config,
span,
source_text,
CandidateParseFailed(error),
)
}
}
Ok({ matches, rejections, candidates_examined })
}