///|
fn is_blank_roman_edit_id(id : String) -> Bool {
let chars = id.to_array()
if chars.length() == 0 {
return true
}
for character in chars {
if !is_outer_whitespace(character) {
return false
}
}
true
}
///|
fn validate_roman_edit_ids(
edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
for index = 0; index < edits.length(); index = index + 1 {
if is_blank_roman_edit_id(edits[index].id) {
return Err(EmptyRomanEditId(index))
}
for previous = 0; previous < index; previous = previous + 1 {
if edits[previous].id == edits[index].id {
return Err(DuplicateRomanEditId(edits[index].id))
}
}
}
Ok(())
}
///|
fn validate_roman_edit_spans(
edits : Array[RomanTextEdit],
source_length : Int,
) -> Result[Unit, RomanEditError] {
for edit in edits {
if edit.span.start < 0 || edit.span.end < 0 {
return Err(NegativeRomanEditSpan(edit.id, edit.span))
}
if edit.span.start > edit.span.end {
return Err(ReversedRomanEditSpan(edit.id, edit.span))
}
if edit.span.end > source_length {
return Err(RomanEditSpanOutOfBounds(edit.id, edit.span, source_length))
}
}
Ok(())
}
///|
fn sort_roman_edits_by_source(
edits : Array[RomanTextEdit],
) -> Array[RomanTextEdit] {
let sorted : Array[RomanTextEdit] = []
for edit in edits {
sorted.push(edit)
let mut index = sorted.length() - 1
while index > 0 && sorted[index - 1].span.start > sorted[index].span.start {
let previous = sorted[index - 1]
sorted[index - 1] = sorted[index]
sorted[index] = previous
index = index - 1
}
}
sorted
}
///|
fn validate_roman_edit_overlap(
edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
for index = 1; index < edits.length(); index = index + 1 {
let previous = edits[index - 1]
let current = edits[index]
let same_start_with_replacement = current.span.start == previous.span.start &&
(
current.span.end > current.span.start ||
previous.span.end > previous.span.start
)
if current.span.start < previous.span.end || same_start_with_replacement {
return Err(OverlappingRomanEdits(previous.id, current.id))
}
}
Ok(())
}
///|
fn validate_roman_edit_sources(
source_chars : Array[Char],
edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
for edit in edits {
let actual = scan_substring(source_chars, edit.span.start, edit.span.end)
if actual != edit.expected_source {
return Err(RomanEditSourceMismatch(edit.id, edit.expected_source, actual))
}
}
Ok(())
}
///|
fn validate_roman_edit_no_ops(
edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
for edit in edits {
if edit.replacement == edit.expected_source {
return Err(NoOpRomanEdit(edit.id))
}
}
Ok(())
}
///|
fn count_unchanged_roman_segments(
source_length : Int,
edits : Array[RomanTextEdit],
) -> Int {
let mut count = 0
let mut cursor = 0
for edit in edits {
if cursor < edit.span.start {
count = count + 1
}
cursor = edit.span.end
}
if cursor < source_length {
count = count + 1
}
count
}
///|
// Array and string lengths are Int-bounded. Even Int.max_value edits whose
// replacements each have Int.max_value characters remain below Int64.max_value.
fn accumulate_roman_edit_length(total : Int64, length : Int) -> Int64 {
total + length.to_int64()
}
///|
fn project_roman_edit_length(
source_length : Int,
removed_length : Int64,
inserted_length : Int64,
) -> Int64 {
source_length.to_int64() - removed_length + inserted_length
}
///|
fn roman_max_representable_output_length() -> Int64 {
2147483647L
}
///|
fn validate_roman_projected_output_representation(
projected_length : Int64,
) -> Result[Unit, RomanEditError] {
let limit = roman_max_representable_output_length()
if projected_length > limit {
Err(RomanProjectedOutputNotRepresentable(projected_length, limit))
} else {
Ok(())
}
}
///|
fn roman_edit_plan_statistics(
source_length : Int,
edits : Array[RomanTextEdit],
) -> RomanEditPlanStatistics {
let mut removed_length = 0L
let mut inserted_length = 0L
for edit in edits {
removed_length = accumulate_roman_edit_length(
removed_length,
edit.span.end - edit.span.start,
)
inserted_length = accumulate_roman_edit_length(
inserted_length,
edit.replacement.to_array().length(),
)
}
{
edit_count: edits.length(),
source_length: source_length.to_int64(),
unchanged_segment_count: count_unchanged_roman_segments(
source_length, edits,
),
removed_length,
inserted_length,
projected_length: project_roman_edit_length(
source_length, removed_length, inserted_length,
),
}
}
///|
/// Validate and source-order a caller-supplied atomic text edit plan.
///
/// Validation order is configuration, IDs, spans, exact source evidence,
/// overlap, no-op replacements, representable output size, and configured size.
pub fn validate_roman_edit_plan(
source : String,
edits : Array[RomanTextEdit],
config : RomanEditPlanConfig,
) -> Result[RomanEditPlan, RomanEditError] {
if config.max_projected_length < 0 {
return Err(InvalidRomanProjectedOutputLimit(config.max_projected_length))
}
match validate_roman_edit_ids(edits) {
Err(error) => return Err(error)
Ok(_) => ()
}
let source_chars = source.to_array()
let source_length = source_chars.length()
match validate_roman_edit_spans(edits, source_length) {
Err(error) => return Err(error)
Ok(_) => ()
}
let sorted = sort_roman_edits_by_source(edits)
match validate_roman_edit_sources(source_chars, sorted) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_roman_edit_overlap(sorted) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_roman_edit_no_ops(sorted) {
Err(error) => return Err(error)
Ok(_) => ()
}
let statistics = roman_edit_plan_statistics(source_length, sorted)
match
validate_roman_projected_output_representation(statistics.projected_length) {
Err(error) => return Err(error)
Ok(_) => ()
}
if statistics.projected_length > config.max_projected_length {
return Err(
RomanProjectedOutputLimitExceeded(
statistics.projected_length,
config.max_projected_length,
),
)
}
Ok({ original: source, config, edits: sorted, statistics, diagnostics: [] })
}