///|
/// Tolerance-aware comparison settings for independently captured traces.
pub struct CompareOptions {
time_tolerance : Int64
include_equal : Bool
ignore_unknown_case : Bool
} derive(Debug, Eq)
///|
/// Aggregate result for a pairwise comparison.
pub struct ComparisonSummary {
signals_left : Int
signals_right : Int
compared_points : Int
equal_points : Int
different_points : Int
missing_left : Int
missing_right : Int
first_difference : Int64?
} derive(Debug, Eq)
///|
/// Create strict comparison defaults.
pub fn default_compare_options() -> CompareOptions {
{ time_tolerance: 0L, include_equal: false, ignore_unknown_case: true }
}
///|
/// Compare two optional values while treating X and x as equivalent.
pub fn comparable_values(
left : Value?,
right : Value?,
ignore_unknown_case : Bool,
) -> Bool {
match (left, right) {
(None, None) => true
(Some(a), Some(b)) =>
if ignore_unknown_case {
values_equal(a.normalized(), b.normalized(), false)
} else {
values_equal(a, b, false)
}
_ => false
}
}
///|
/// Return the nearest sample at or around a timestamp.
pub fn nearest_value(
trace : Trace,
signal_id : String,
timestamp : Int64,
tolerance : Int64,
) -> Value? {
let safe = Int64::max(0L, tolerance)
let mut result : Value? = None
let mut best_distance : Int64? = None
for change in signal_changes(trace, signal_id) {
let distance = (change.time - timestamp).abs()
if distance <= safe {
match best_distance {
Some(current) if distance >= current => ()
_ => {
best_distance = Some(distance)
result = Some(change.value)
}
}
}
}
result
}
///|
/// Compare all observed timestamps after nearest-sample alignment.
pub fn compare_traces(
left : Trace,
right : Trace,
options : CompareOptions,
) -> ComparisonSummary {
let ids = union_signal_ids(left, right)
let mut equal = 0
let mut different = 0
let mut missing_left = 0
let mut missing_right = 0
let mut first : Int64? = None
for signal_id in ids {
let points = diff_times(left, right, signal_id)
let left_declared = left.has_signal(signal_id)
let right_declared = right.has_signal(signal_id)
if !left_declared {
missing_left += points.length()
continue
}
if !right_declared {
missing_right += points.length()
continue
}
for timestamp in points {
let left_value = nearest_value(
left,
signal_id,
timestamp,
options.time_tolerance,
)
let right_value = nearest_value(
right,
signal_id,
timestamp,
options.time_tolerance,
)
if comparable_values(left_value, right_value, options.ignore_unknown_case) {
equal += 1
} else {
different += 1
if first is None {
first = Some(timestamp)
}
}
}
}
{
signals_left: left.signals.length(),
signals_right: right.signals.length(),
compared_points: equal + different,
equal_points: equal,
different_points: different,
missing_left,
missing_right,
first_difference: first,
}
}
///|
/// Return the equality ratio of two traces.
pub fn comparison_ratio(summary : ComparisonSummary) -> Double {
if summary.compared_points == 0 {
1.0
} else {
summary.equal_points.to_double() / summary.compared_points.to_double()
}
}
///|
/// Return whether two traces meet the requested comparison policy.
pub fn comparison_passes(summary : ComparisonSummary) -> Bool {
summary.different_points == 0 &&
summary.missing_left == 0 &&
summary.missing_right == 0
}
///|
/// Render a comparison summary as a stable line.
pub fn comparison_summary_line(summary : ComparisonSummary) -> String {
"left_signals=" +
summary.signals_left.to_string() +
" right_signals=" +
summary.signals_right.to_string() +
" compared=" +
summary.compared_points.to_string() +
" equal=" +
summary.equal_points.to_string() +
" different=" +
summary.different_points.to_string() +
" missing_left=" +
summary.missing_left.to_string() +
" missing_right=" +
summary.missing_right.to_string() +
" ratio=" +
comparison_ratio(summary).to_string()
}
///|
/// Render a comparison summary as JSON.
pub fn comparison_summary_json(summary : ComparisonSummary) -> String {
let first = match summary.first_difference {
Some(value) => value.to_string()
None => "null"
}
"{\"signals_left\":" +
summary.signals_left.to_string() +
",\"signals_right\":" +
summary.signals_right.to_string() +
",\"compared\":" +
summary.compared_points.to_string() +
",\"equal\":" +
summary.equal_points.to_string() +
",\"different\":" +
summary.different_points.to_string() +
",\"missing_left\":" +
summary.missing_left.to_string() +
",\"missing_right\":" +
summary.missing_right.to_string() +
",\"first_difference\":" +
first +
"}"
}
///|
/// Compare only declarations and signal paths.
pub fn declaration_compatible(left : Trace, right : Trace) -> Bool {
if left.signals.length() != right.signals.length() {
return false
}
for signal in left.signals {
match find_signal(right, signal.id) {
Some(other) =>
if signal.width != other.width ||
signal_path(signal) != signal_path(other) {
return false
}
None => return false
}
}
true
}