///|
/// Shared validation and normalization primitives for engineering inputs.
pub(all) struct ValidationIssue {
field : String
code : String
message : String
value : Double
blocking : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ValidationSummary {
checked : Int
passed : Int
issues : Array[ValidationIssue]
valid : Bool
completeness : Double
} derive(Debug, Eq, ToJson)
///|
pub fn validation_issue(
field : String,
code : String,
message : String,
value : Double,
blocking : Bool,
) -> ValidationIssue {
{ field, code, message, value, blocking }
}
///|
pub fn check_finite(field : String, value : Double) -> ValidationIssue? {
if value.is_nan() || value.is_inf() {
Some(
validation_issue(field, "non-finite", "value must be finite", value, true),
)
} else {
None
}
}
///|
pub fn check_positive(field : String, value : Double) -> ValidationIssue? {
if value <= 0.0 {
Some(
validation_issue(
field, "non-positive", "value must be greater than zero", value, true,
),
)
} else {
None
}
}
///|
pub fn check_non_negative(field : String, value : Double) -> ValidationIssue? {
if value < 0.0 {
Some(
validation_issue(
field, "negative", "value cannot be negative", value, true,
),
)
} else {
None
}
}
///|
pub fn check_fraction(field : String, value : Double) -> ValidationIssue? {
if value < 0.0 || value > 1.0 {
Some(
validation_issue(
field, "fraction", "value must lie between zero and one", value, true,
),
)
} else {
None
}
}
///|
pub fn check_range(
field : String,
value : Double,
lower : Double,
upper : Double,
) -> ValidationIssue? {
if value < lower || value > upper {
Some(
validation_issue(
field, "range", "value is outside the allowed range", value, true,
),
)
} else {
None
}
}
///|
pub fn check_close(
field : String,
value : Double,
reference : Double,
tolerance : Double,
) -> ValidationIssue? {
if (value - reference).abs() > tolerance {
Some(
validation_issue(
field, "mismatch", "value is not within tolerance", value, false,
),
)
} else {
None
}
}
///|
pub fn summarize_validation(
checked : Int,
issues : ArrayView[ValidationIssue],
) -> ValidationSummary {
let mut blocking = 0
for issue in issues {
if issue.blocking {
blocking += 1
}
}
{
checked,
passed: (checked - issues.length()).max(0),
issues: issues.to_owned(),
valid: blocking == 0,
completeness: if checked <= 0 {
0.0
} else {
(checked - issues.length()).max(0).to_double() / checked.to_double()
},
}
}
///|
pub fn validate_vector(
field_prefix : String,
values : ArrayView[Double],
lower : Double,
upper : Double,
) -> ValidationSummary {
let issues = []
for index in 0.. issues.push(issue)
None =>
match
check_range(
field_prefix + "[" + index.to_string() + "]",
values[index],
lower,
upper,
) {
Some(issue) => issues.push(issue)
None => ()
}
}
}
summarize_validation(values.length(), issues)
}
///|
pub fn normalize_to_range(
value : Double,
lower : Double,
upper : Double,
) -> Double raise ExtractionError {
if lower >= upper {
raise ExtractionError::InvalidArgument(
message="normalization range is invalid",
)
}
(value - lower) / (upper - lower)
}
///|
pub fn denormalize_from_range(
normalized : Double,
lower : Double,
upper : Double,
) -> Double raise ExtractionError {
if lower >= upper {
raise ExtractionError::InvalidArgument(
message="denormalization range is invalid",
)
}
lower + normalized * (upper - lower)
}
///|
pub fn clamp(value : Double, lower : Double, upper : Double) -> Double {
value.max(lower).min(upper)
}
///|
pub fn clamp_vector(
values : ArrayView[Double],
lower : Double,
upper : Double,
) -> Array[Double] {
[
for value in values => clamp(value, lower, upper)
]
}
///|
pub fn relative_difference(value : Double, reference : Double) -> Double {
if reference == 0.0 {
value.abs()
} else {
(value - reference) / reference.abs()
}
}
///|
pub fn percent_difference(value : Double, reference : Double) -> Double {
100.0 * relative_difference(value, reference)
}
///|
pub fn all_finite(values : ArrayView[Double]) -> Bool {
for value in values {
if value.is_nan() || value.is_inf() {
return false
}
}
true
}
///|
pub fn all_in_range(
values : ArrayView[Double],
lower : Double,
upper : Double,
) -> Bool {
for value in values {
if value < lower || value > upper {
return false
}
}
true
}
///|
pub fn sum_non_negative(
values : ArrayView[Double],
) -> Double raise ExtractionError {
let mut total = 0.0
for value in values {
if value < 0.0 {
raise ExtractionError::InvalidArgument(
message="negative value in non-negative sum",
)
}
total += value
}
total
}
///|
pub fn normalized_weights(
weights : ArrayView[Double],
) -> Array[Double] raise ExtractionError {
let total = sum_non_negative(weights)
if total == 0.0 {
raise ExtractionError::InvalidArgument(message="weights sum to zero")
}
[
for weight in weights => weight / total
]
}
///|
pub fn weighted_average(
values : ArrayView[Double],
weights : ArrayView[Double],
) -> Double raise ExtractionError {
if values.length() != weights.length() || values.length() == 0 {
raise ExtractionError::InvalidArgument(
message="weighted average arrays are invalid",
)
}
let normalized = normalized_weights(weights)
let mut total = 0.0
for index in 0.. Double raise ExtractionError {
let mean = weighted_average(values, weights)
let normalized = normalized_weights(weights)
let mut total = 0.0
for index in 0.. String {
let lines = [
"| Field | Code | Message | Value | Blocking |", "|---|---|---|---:|---|",
]
for issue in issues {
lines.push(
"| " +
issue.field +
" | " +
issue.code +
" | " +
issue.message +
" | " +
issue.value.to_string() +
" | " +
issue.blocking.to_string() +
" |",
)
}
lines.join("\n")
}
///|
pub fn validation_summary_text(summary : ValidationSummary) -> String {
"checked=" +
summary.checked.to_string() +
",passed=" +
summary.passed.to_string() +
",issues=" +
summary.issues.length().to_string() +
",valid=" +
summary.valid.to_string() +
",completeness=" +
summary.completeness.to_string()
}
///|
pub fn merge_validation_summaries(
first : ValidationSummary,
second : ValidationSummary,
) -> ValidationSummary {
let issues = first.issues.copy()
issues.append(second.issues)
summarize_validation(first.checked + second.checked, issues)
}
///|
pub fn require_valid(
summary : ValidationSummary,
) -> Result[Unit, Array[ValidationIssue]] {
if summary.valid {
Ok(())
} else {
Err(summary.issues)
}
}