///|
/// Facts proven by validating one structured conversion result.
pub struct ConversionIntegrity {
source_scalar_value : Int
covered_scalar_value : Int
dropped_scalar_value : Int
token_value : Int
candidate_value : Int
diagnostic_value : Int
} derive(Eq, Debug)
///|
fn validate_token_decision(token : ConvertedToken) -> Result[Unit, PinyinError] {
match (token.kind(), token.decision_source()) {
(PinyinToken, PhraseLexicon) | (PinyinToken, UnicodeReading) => Ok(())
(LiteralToken, UnknownPreserved) => Ok(())
_ => Err(InvalidConversionResult("token_decision_mismatch"))
}
}
///|
fn validate_token_candidates(
token : ConvertedToken,
) -> Result[Int, PinyinError] {
let candidates = token.candidates()
if token.kind() == PinyinToken && candidates.length() == 0 {
return Err(InvalidConversionResult("pinyin_token_without_candidates"))
}
if token.kind() == LiteralToken && candidates.length() > 0 {
return Err(InvalidConversionResult("literal_token_with_candidates"))
}
for candidate in candidates {
match parse_syllable(candidate) {
Err(_) =>
return Err(
InvalidConversionResult("unparseable_candidate:" + candidate),
)
Ok(_) => ()
}
}
Ok(candidates.length())
}
///|
/// Validates spans, token decisions, and candidate syllables without rerunning conversion.
pub fn validate_conversion_result(
result : ConversionResult,
) -> Result[ConversionIntegrity, PinyinError] {
let source_count = text_code_points(result.source()).length()
let tokens = result.tokens()
let mut previous_end = 0
let mut covered = 0
let mut candidates = 0
for token in tokens {
let span = token.span()
if span.length() <= 0 {
return Err(InvalidConversionResult("empty_token_span"))
}
if span.start() < previous_end {
return Err(InvalidConversionResult("overlapping_or_unsorted_spans"))
}
if span.end() > source_count {
return Err(InvalidConversionResult("span_outside_source"))
}
match validate_token_decision(token) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_token_candidates(token) {
Err(error) => return Err(error)
Ok(count) => candidates = candidates + count
}
covered = covered + span.length()
previous_end = span.end()
}
if covered > source_count {
return Err(InvalidConversionResult("covered_scalars_exceed_source"))
}
Ok({
source_scalar_value: source_count,
covered_scalar_value: covered,
dropped_scalar_value: source_count - covered,
token_value: tokens.length(),
candidate_value: candidates,
diagnostic_value: result.diagnostics().length(),
})
}
///|
pub fn ConversionIntegrity::source_scalars(self : ConversionIntegrity) -> Int {
self.source_scalar_value
}
///|
pub fn ConversionIntegrity::covered_scalars(self : ConversionIntegrity) -> Int {
self.covered_scalar_value
}
///|
pub fn ConversionIntegrity::dropped_scalars(self : ConversionIntegrity) -> Int {
self.dropped_scalar_value
}
///|
pub fn ConversionIntegrity::tokens(self : ConversionIntegrity) -> Int {
self.token_value
}
///|
pub fn ConversionIntegrity::validated_candidates(
self : ConversionIntegrity,
) -> Int {
self.candidate_value
}
///|
pub fn ConversionIntegrity::diagnostics(self : ConversionIntegrity) -> Int {
self.diagnostic_value
}
///|
pub fn ConversionIntegrity::has_full_coverage(
self : ConversionIntegrity,
) -> Bool {
self.dropped_scalar_value == 0
}