///|
pub(all) struct ComparisonIssue {
code : String
message : String
spectrum : Int32
point : Int32
} derive(@debug.Debug, ToJson)
///|
pub(all) struct Comparison {
equivalent : Bool
compared_points : Int32
issue_count : Int32
truncated : Bool
issues : Array[ComparisonIssue]
} derive(@debug.Debug, ToJson)
///|
pub extend ComparisonIssue with @debug.Debug::{to_repr}
///|
pub extend ComparisonIssue with ToJson::{to_json}
///|
pub extend Comparison with @debug.Debug::{to_repr}
///|
pub extend Comparison with ToJson::{to_json}
///|
pub fn compare_spectra(
left : Array[Spectrum],
right : Array[Spectrum],
options~ : ParseOptions = ParseOptions::default(),
compare_records~ : Bool = false,
max_issues~ : Int32 = 100,
) -> Comparison raise JcampError {
check_options(options)
guard max_issues >= 1 && max_issues <= 10000 else {
reject("COMPARE_LIMIT", "max_issues must be between 1 and 10000")
}
// Validate aggregate budgets before walking two externally constructed lists.
ignore(export_json(left))
ignore(export_json(right))
let issues : Array[ComparisonIssue] = []
let mut issue_count = 0
let mut compared_points = 0
fn issue(
code : String,
message : String,
spectrum : Int32,
point : Int32,
) -> Unit {
issue_count += 1
if issues.length() < max_issues {
issues.push({ code, message, spectrum, point, })
}
}
if left.length() != right.length() {
issue("COMPARE_SPECTRA", "spectrum counts differ", -1, -1)
}
let count = if left.length() < right.length() {
left.length()
} else {
right.length()
}
for i = 0; i < count; i = i + 1 {
let a = left[i]
let b = right[i]
if a.title != b.title {
issue("COMPARE_TITLE", "spectrum titles differ", i, -1)
}
if canonical_label(a.data_type) != canonical_label(b.data_type) {
issue("COMPARE_TYPE", "spectrum types differ", i, -1)
}
if canonical_label(a.x_units) != canonical_label(b.x_units) ||
canonical_label(a.y_units) != canonical_label(b.y_units) {
issue(
"COMPARE_UNITS", "axis units differ; conversion must be explicit", i, -1,
)
}
if a.points.length() != b.points.length() {
issue("COMPARE_POINTS", "point counts differ", i, -1)
}
if compare_records {
let derived = [
"TITLE", "JCAMPDX", "DATATYPE", "DATACLASS", "NPOINTS", "FIRSTX", "LASTX",
"FIRSTY", "LASTY", "MINX", "MAXX", "MINY", "MAXY", "DELTAX", "XFACTOR", "YFACTOR",
"XUNITS", "YUNITS",
]
fn descriptors(records : Array[Record]) -> Array[(String, String)] {
records
.filter(r => !derived.contains(canonical_label(r.label)))
.map(r => (canonical_label(r.label), trim(r.value)))
}
if descriptors(a.records) != descriptors(b.records) {
issue("COMPARE_RECORDS", "ordered descriptive records differ", i, -1)
}
}
let points = if a.points.length() < b.points.length() {
a.points.length()
} else {
b.points.length()
}
for j = 0; j < points; j = j + 1 {
compared_points += 1
if !close(a.points[j].x, b.points[j].x, options) {
issue("COMPARE_X", "X coordinates differ", i, j)
}
if !matching_y(a.points[j].y, b.points[j].y, options) {
issue("COMPARE_Y", "ordinates or missing-value positions differ", i, j)
}
}
}
{
equivalent: issue_count == 0,
compared_points,
issue_count,
truncated: issue_count > issues.length(),
issues,
}
}
///|
pub fn compare_documents(
left : String,
right : String,
options~ : ParseOptions = ParseOptions::default(),
compare_records~ : Bool = false,
max_issues~ : Int32 = 100,
) -> Comparison raise JcampError {
compare_spectra(
read_spectra(left, options~),
read_spectra(right, options~),
options~,
compare_records~,
max_issues~,
)
}