///|
/// Structured input-contract issue used by production pipelines.
pub enum ValidationSeverity {
Info
Warning
Error
}
///|
pub struct ValidationIssue {
code : String
field : String
message : String
severity : ValidationSeverity
index : Int
}
///|
pub struct ValidationReport {
name : String
checked : Int
passed : Int
failed : Int
issues : Array[ValidationIssue]
score : Double
}
///|
pub fn validation_issue(
code : String,
field : String,
message : String,
severity : ValidationSeverity,
index : Int,
) -> ValidationIssue {
{ code, field, message, severity, index }
}
///|
pub fn validation_empty_report(name : String) -> ValidationReport {
{ name, checked: 0, passed: 0, failed: 0, issues: [], score: 1.0 }
}
///|
fn validation_add(
report : ValidationReport,
issue : ValidationIssue,
passed : Bool,
) -> ValidationReport {
report.issues.push(issue)
let checked = report.checked + 1
let failed = if passed { report.failed } else { report.failed + 1 }
let success = if passed { report.passed + 1 } else { report.passed }
{
..report,
checked,
passed: success,
failed,
score: success.to_double() / checked.to_double(),
}
}
///|
pub fn validation_severity_name(severity : ValidationSeverity) -> String {
match severity {
Info => "info"
Warning => "warning"
Error => "error"
}
}
///|
pub fn validation_report_lines(report : ValidationReport) -> Array[String] {
let result = [
"name=" + report.name,
"checked=" + report.checked.to_string(),
"passed=" + report.passed.to_string(),
"failed=" + report.failed.to_string(),
"score=" + report.score.to_string(),
]
for issue in report.issues {
result.push(
issue.code +
"|" +
issue.field +
"|" +
validation_severity_name(issue.severity) +
"|" +
issue.message,
)
}
result
}
///|
pub fn validation_report_string(report : ValidationReport) -> String {
validation_report_lines(report).join("\n")
}
///|
pub fn validation_has_errors(report : ValidationReport) -> Bool {
for issue in report.issues {
match issue.severity {
Error => return true
_ => ()
}
}
false
}
///|
pub fn validation_missing(
data : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
for index = 0; index < data.length(); index = index + 1 {
let missing = quality_is_missing(data[index])
report = validation_add(
report,
validation_issue(
"missing",
"value",
"missing numeric observation",
Error,
index,
),
!missing,
)
}
report
}
///|
pub fn validation_finite(
data : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
for index = 0; index < data.length(); index = index + 1 {
let finite = !quality_is_missing(data[index]) &&
data[index] < 1.0e308 &&
data[index] > -1.0e308
report = validation_add(
report,
validation_issue("finite", "value", "value must be finite", Error, index),
finite,
)
}
report
}
///|
pub fn validation_range(
data : Array[Double],
lower : Double,
upper : Double,
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
for index = 0; index < data.length(); index = index + 1 {
let valid = !quality_is_missing(data[index]) &&
data[index] >= lower &&
data[index] <= upper
report = validation_add(
report,
validation_issue(
"range",
"value",
"value is outside configured bounds",
Error,
index,
),
valid,
)
}
report
}
///|
pub fn validation_monotone(
data : Array[Double],
increasing : Bool,
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
if data.length() < 2 {
return report
}
for index = 1; index < data.length(); index = index + 1 {
let valid = if increasing {
data[index] >= data[index - 1]
} else {
data[index] <= data[index - 1]
}
report = validation_add(
report,
validation_issue(
"monotone",
"sequence",
"sequence order violated",
Warning,
index,
),
valid,
)
}
report
}
///|
pub fn validation_length(
left : Array[Double],
right : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
let valid = left.length() == right.length()
report = validation_add(
report,
validation_issue(
"length",
"series",
"paired series have different lengths",
Error,
-1,
),
valid,
)
report
}
///|
pub fn validation_pairs(
x : Array[Double],
y : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_length(x, y, name)
let count = if x.length() < y.length() { x.length() } else { y.length() }
for index = 0; index < count; index = index + 1 {
let valid = !quality_is_missing(x[index]) && !quality_is_missing(y[index])
report = validation_add(
report,
validation_issue(
"pair",
"observation",
"paired value is missing",
Error,
index,
),
valid,
)
}
report
}
///|
pub fn validation_weights(
weights : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
for index = 0; index < weights.length(); index = index + 1 {
let valid = !quality_is_missing(weights[index]) && weights[index] >= 0.0
report = validation_add(
report,
validation_issue(
"weight",
"weight",
"weight must be non-negative",
Error,
index,
),
valid,
)
}
let mut total = 0.0
for weight in weights {
total += weight
}
report = validation_add(
report,
validation_issue(
"weight-total",
"weights",
"weight total must be positive",
Error,
-1,
),
total > 0.0,
)
report
}
///|
pub fn validation_probability(
probabilities : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
for index = 0; index < probabilities.length(); index = index + 1 {
let valid = probabilities[index] >= 0.0 &&
probabilities[index] <= 1.0 &&
!quality_is_missing(probabilities[index])
report = validation_add(
report,
validation_issue(
"probability",
"probability",
"probability must be in [0,1]",
Error,
index,
),
valid,
)
}
report
}
///|
pub fn validation_matrix(
matrix : Array[Array[Double]],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
let width = if matrix.length() == 0 { 0 } else { matrix[0].length() }
report = validation_add(
report,
validation_issue(
"matrix-width",
"matrix",
"matrix rows have inconsistent width",
Error,
-1,
),
width > 0 || matrix.length() == 0,
)
for row_index = 0; row_index < matrix.length(); row_index = row_index + 1 {
let row = matrix[row_index]
let valid = row.length() == width
report = validation_add(
report,
validation_issue(
"matrix-row",
"matrix",
"matrix row width mismatch",
Error,
row_index,
),
valid,
)
for value in row {
report = validation_add(
report,
validation_issue(
"matrix-finite",
"matrix",
"matrix value is missing",
Error,
row_index,
),
!quality_is_missing(value),
)
}
}
report
}
///|
pub fn validation_time_axis(
times : Array[Double],
strict : Bool,
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
if times.length() < 2 {
return report
}
for index = 1; index < times.length(); index = index + 1 {
let valid = if strict {
times[index] > times[index - 1]
} else {
times[index] >= times[index - 1]
}
report = validation_add(
report,
validation_issue(
"time-order",
"timestamp",
"timestamps are not ordered",
Error,
index,
),
valid,
)
}
report
}
///|
pub fn validation_duplicates(
data : Array[Double],
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
let duplicates = duplicate_indices(data)
for index in duplicates {
report = validation_add(
report,
validation_issue(
"duplicate",
"value",
"duplicate observation",
Warning,
index,
),
false,
)
}
if data.length() == 0 {
report
} else {
validation_add(
report,
validation_issue(
"duplicate-rate",
"series",
"duplicate rate is measurable",
Info,
-1,
),
true,
)
}
}
///|
pub fn validation_consistency(
data : Array[Double],
rule : QualityRule,
name : String,
) -> ValidationReport {
let mut report = validation_empty_report(name)
let flags = quality_valid_flags(data, rule)
for index = 0; index < flags.length(); index = index + 1 {
report = validation_add(
report,
validation_issue(
"quality",
"value",
"quality rule violated",
Warning,
index,
),
flags[index],
)
}
report
}
///|
pub fn validation_merge(
first : ValidationReport,
second : ValidationReport,
) -> ValidationReport {
let issues = first.issues.copy()
for issue in second.issues {
issues.push(issue)
}
let checked = first.checked + second.checked
let passed = first.passed + second.passed
{
name: first.name + "+" + second.name,
checked,
passed,
failed: first.failed + second.failed,
issues,
score: if checked == 0 {
1.0
} else {
passed.to_double() / checked.to_double()
},
}
}
///|
pub fn validation_batch_score(reports : Array[ValidationReport]) -> Double {
let mut checked = 0
let mut passed = 0
for report in reports {
checked += report.checked
passed += report.passed
}
if checked == 0 {
1.0
} else {
passed.to_double() / checked.to_double()
}
}
///|
pub fn validation_summary(
data : Array[Double],
name : String,
) -> ValidationReport {
validation_merge(
validation_finite(data, name),
validation_duplicates(data, name),
)
}