///|
fn interpret_normalized_value(text : String) -> Int? {
let chars = text.to_array()
if chars.length() == 0 {
return None
}
let mut total = 0
let mut index = 0
while index < chars.length() {
let current = match symbol_value(chars[index]) {
Some(value) => value
None => return None
}
if index + 1 < chars.length() {
let next = match symbol_value(chars[index + 1]) {
Some(value) => value
None => return None
}
if current < next {
total = total + next - current
index = index + 2
continue
}
}
total = total + current
index = index + 1
}
Some(total)
}
///|
fn normalized_index_span(
input : NormalizedRomanInput,
normalized_index : Int,
width : Int,
) -> SourceSpan {
let mut offset = 0
let mut start = input.content_span.start
let mut end = input.content_span.end
let target_end = normalized_index + width
let mut found_start = false
for unit in input.units {
let unit_width = unit.normalized_text.to_array().length()
let unit_end = offset + unit_width
if !found_start && normalized_index < unit_end {
start = unit.span.start
found_start = true
}
if found_start && target_end <= unit_end {
end = unit.span.end
return { start, end }
}
offset = unit_end
}
{ start, end }
}
///|
fn unit_token_role(text : String) -> RomanTokenRole {
let chars = text.to_array()
if chars.length() == 2 {
match (symbol_value(chars[0]), symbol_value(chars[1])) {
(Some(left), Some(right)) =>
if left < right {
SubtractiveToken
} else {
SymbolToken
}
_ => SymbolToken
}
} else {
SymbolToken
}
}
///|
fn token_from_unit(unit : NormalizedRomanUnit) -> RomanToken {
let value = match interpret_normalized_value(unit.normalized_text) {
Some(number) => number
None => 0
}
{
span: unit.span,
source_text: unit.source_text,
normalized_text: unit.normalized_text,
value,
role: unit_token_role(unit.normalized_text),
}
}
///|
fn build_report_tokens(input : NormalizedRomanInput) -> Array[RomanToken] {
let tokens : Array[RomanToken] = []
let mut index = 0
while index < input.units.length() {
let current = input.units[index]
if current.normalized_text == "(" {
tokens.push({
span: current.span,
source_text: current.source_text,
normalized_text: "(",
value: 0,
role: GroupOpenToken,
})
index = index + 1
continue
}
if current.normalized_text == ")" {
tokens.push({
span: current.span,
source_text: current.source_text,
normalized_text: ")",
value: 0,
role: GroupCloseToken,
})
index = index + 1
continue
}
if current.normalized_text.to_array().length() == 1 &&
index + 1 < input.units.length() &&
input.units[index + 1].normalized_text.to_array().length() == 1 {
let next = input.units[index + 1]
let left_chars = current.normalized_text.to_array()
let right_chars = next.normalized_text.to_array()
if is_subtractive_pair(left_chars[0], right_chars[0]) {
let left_value = symbol_value(left_chars[0]).unwrap()
let right_value = symbol_value(right_chars[0]).unwrap()
tokens.push({
span: { start: current.span.start, end: next.span.end },
source_text: current.source_text + next.source_text,
normalized_text: current.normalized_text + next.normalized_text,
value: right_value - left_value,
role: SubtractiveToken,
})
index = index + 2
continue
}
}
tokens.push(token_from_unit(current))
index = index + 1
}
tokens
}
///|
fn parse_error_span(
input : NormalizedRomanInput,
index : Int,
width : Int,
) -> SourceSpan {
normalized_index_span(input, index, width)
}
///|
fn map_legacy_parse_error(
input : NormalizedRomanInput,
error : ParseError,
) -> RomanReportError {
match error {
EmptyInput => EmptyRomanInput
InvalidCharacter(index, character) =>
UnsupportedRomanCharacter(parse_error_span(input, index, 1), character)
InvalidRepetition(index, _) =>
InvalidRomanGrammar(
parse_error_span(input, index, 1),
InvalidRepetitionCode,
)
InvalidSubtractivePair(index, _, _) =>
InvalidRomanGrammar(
parse_error_span(input, index, 2),
InvalidSubtractionCode,
)
InvalidOrder(index) =>
InvalidRomanGrammar(parse_error_span(input, index, 1), InvalidOrderCode)
NonCanonical(expected) => NonCanonicalRoman(expected)
ParseOutOfRange(value) => RomanReportOutOfRange(value)
}
}
///|
fn parse_profile_candidate(
input : NormalizedRomanInput,
config : ParseConfig,
) -> Result[Int, RomanReportError] {
match config.mode {
ModernCanonical =>
match parse(input.normalized) {
Ok(value) => Ok(value)
Err(error) => Err(map_legacy_parse_error(input, error))
}
AdditiveHistorical | ClockFace =>
match interpret_normalized_value(input.normalized) {
None =>
Err(InvalidRomanGrammar(input.content_span, UnsupportedCharacterCode))
Some(value) => Ok(value)
}
ParenthesizedThousands => parse_extended_candidate(input)
}
}
///|
fn report_format_config(config : ParseConfig) -> FormatConfig {
{ mode: config.mode, letter_case: Uppercase }
}
///|
fn report_notices(input : NormalizedRomanInput) -> Array[String] {
let notices : Array[String] = []
if input.used_unicode_compatibility {
notices.push("Unicode Roman compatibility characters expanded to ASCII")
}
if input.trimmed_outer_whitespace {
notices.push("outer whitespace removed by configured policy")
}
notices
}
///|
/// Parse configured Roman input and return source-located evidence.
pub fn parse_with_config(
input : String,
config : ParseConfig,
) -> Result[ParseReport, RomanReportError] {
let normalized = match normalize_for_report(input, config) {
Ok(value) => value
Err(error) => return Err(error)
}
let value = match parse_profile_candidate(normalized, config) {
Ok(number) => number
Err(error) => return Err(error)
}
let canonical = match
format_with_config(value, report_format_config(config)) {
Ok(text) => text
Err(_) => return Err(RomanReportOutOfRange(value))
}
if normalized.normalized != canonical {
return Err(NonCanonicalRoman(canonical))
}
Ok({
original: normalized.original,
normalized: normalized.normalized,
value,
canonical,
tokens: build_report_tokens(normalized),
diagnostics: [],
notices: report_notices(normalized),
used_unicode_compatibility: normalized.used_unicode_compatibility,
trimmed_outer_whitespace: normalized.trimmed_outer_whitespace,
})
}