///|
/// Compare doubles without relying on a total-order implementation.
pub fn compare_double(left : Double, right : Double) -> Int {
if left < right {
-1
} else if left > right {
1
} else {
0
}
}
///|
/// Report whether a floating-point value is a usable finite input.
pub fn is_finite_value(value : Double) -> Bool {
value == value && value.abs() < 1.7976931348623157e308
}
///|
fn issue(
issues : Array[ValidationIssue],
code : ValidationCode,
index : Int,
message : String,
) -> Unit {
issues.push({ code, index, message })
}
///|
/// Validate observations without changing caller-owned data.
pub fn validate_observations(
observations : Array[Observation],
) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if observations.length() == 0 {
issue(issues, EmptyInput, 0, "observation input is empty")
}
let mut previous_timestamp = -1.0
let mut previous_distance = -1.0
for index, observation in observations {
if !is_finite_value(observation.timestamp) ||
!is_finite_value(observation.distance) ||
!is_finite_value(observation.elapsed) {
issue(
issues,
NonFiniteValue,
index,
"observation contains a non-finite value",
)
}
if observation.timestamp < 0.0 {
issue(issues, NegativeTimestamp, index, "timestamp must not be negative")
}
if observation.distance < 0.0 {
issue(issues, NegativeDistance, index, "distance must not be negative")
}
if index > 0 && observation.timestamp < previous_timestamp {
issue(
issues,
NonMonotonicTime,
index,
"timestamps must be non-decreasing",
)
}
if index > 0 && observation.distance < previous_distance {
issue(
issues,
NonMonotonicDistance,
index,
"distance must be non-decreasing",
)
}
previous_timestamp = observation.timestamp
previous_distance = observation.distance
}
{ valid: issues.length() == 0, checked: observations.length(), issues }
}
///|
/// Validate a route profile and its segment parameter ranges.
pub fn validate_route(route : RouteProfile) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if route.segments.length() == 0 {
issue(issues, EmptyInput, 0, "route must contain at least one segment")
}
for index, segment in route.segments {
if segment.distance <= 0.0 || !is_finite_value(segment.distance) {
issue(issues, InvalidRange, index, "segment distance must be positive")
}
if segment.free_flow_speed <= 0.0 ||
segment.typical_speed <= 0.0 ||
segment.rush_hour_speed <= 0.0 {
issue(issues, InvalidRange, index, "segment speeds must be positive")
}
if segment.reliability < 0.0 || segment.reliability > 1.0 {
issue(issues, InvalidRange, index, "reliability must be within [0, 1]")
}
if segment.segment_id.trim().is_empty() {
issue(issues, InvalidConfiguration, index, "segment id must not be blank")
}
}
{ valid: issues.length() == 0, checked: route.segments.length(), issues }
}
///|
/// Clamp a configuration to safe operational limits.
pub fn sanitize_kalman_config(config : KalmanConfig) -> KalmanConfig {
{
process_noise: if config.process_noise < 0.0 {
0.0
} else {
config.process_noise
},
measurement_noise: if config.measurement_noise < 0.000001 {
0.000001
} else {
config.measurement_noise
},
initial_error: if config.initial_error < 0.000001 {
0.000001
} else {
config.initial_error
},
min_variance: if config.min_variance < 0.0000001 {
0.0000001
} else {
config.min_variance
},
gate_sigma: if config.gate_sigma < 0.1 {
0.1
} else {
config.gate_sigma
},
}
}
///|
/// Clamp a window configuration while preserving its ordering invariants.
pub fn sanitize_window_config(config : WindowConfig) -> WindowConfig {
let min_size = if config.min_size < 1 { 1 } else { config.min_size }
let max_size = if config.max_size < min_size {
min_size
} else {
config.max_size
}
{
min_size,
max_size,
low_variance: if config.low_variance < 0.0 {
0.0
} else {
config.low_variance
},
high_variance: if config.high_variance < config.low_variance {
config.low_variance
} else {
config.high_variance
},
expansion_step: if config.expansion_step < 1 {
1
} else {
config.expansion_step
},
shrink_step: if config.shrink_step < 1 {
1
} else {
config.shrink_step
},
}
}
///|
/// Sort observations and keep the last value for a duplicate timestamp.
pub fn normalize_observations(
observations : Array[Observation],
) -> Array[Observation] {
let sorted = observations.copy()
sorted.sort_by((left, right) => {
let time_order = compare_double(left.timestamp, right.timestamp)
if time_order == 0 {
compare_double(left.elapsed, right.elapsed)
} else {
time_order
}
})
let normalized : Array[Observation] = []
for observation in sorted {
if normalized.length() > 0 &&
normalized[normalized.length() - 1].timestamp == observation.timestamp {
normalized[normalized.length() - 1] = observation
} else {
normalized.push(observation)
}
}
normalized
}
///|
/// Build a normalized batch and count rows discarded by basic validity checks.
pub fn build_observation_batch(
observations : Array[Observation],
source? : String = "memory",
) -> ObservationBatch {
let valid : Array[Observation] = []
let mut dropped = 0
for observation in observations {
if is_finite_value(observation.timestamp) &&
is_finite_value(observation.distance) &&
is_finite_value(observation.elapsed) &&
observation.timestamp >= 0.0 &&
observation.distance >= 0.0 {
valid.push(observation)
} else {
dropped += 1
}
}
{ observations: normalize_observations(valid), source, dropped }
}
///|
/// Validate a calendar bucket range.
pub fn validate_calendar_bucket(bucket : CalendarBucket) -> Bool {
bucket.start_second >= 0.0 &&
bucket.end_second <= 86400.0 &&
bucket.end_second > bucket.start_second &&
bucket.congestion >= 0.0 &&
bucket.congestion <= 1.0
}
///|
/// Validate a complete ETA configuration.
pub fn validate_eta_config(config : EtaConfig) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if config.min_speed <= 0.0 || config.max_speed < config.min_speed {
issue(issues, InvalidConfiguration, 0, "speed bounds are invalid")
}
if config.history_weight < 0.0 || config.live_weight < 0.0 {
issue(issues, InvalidConfiguration, 0, "weights must be non-negative")
}
if config.reliability_floor < 0.0 || config.reliability_floor > 1.0 {
issue(
issues,
InvalidConfiguration,
0,
"reliability floor is outside [0, 1]",
)
}
{ valid: issues.length() == 0, checked: 1, issues }
}