///|
/// Return half the bilateral tolerance width.
pub fn tolerance_half_width(feature : Feature) -> Double {
  (feature.upper - feature.lower).abs() / 2.0
}

///|
/// Return the absolute lower measurement limit.
pub fn lower_limit(feature : Feature) -> Double {
  feature.nominal + feature.lower
}

///|
/// Return the absolute upper measurement limit.
pub fn upper_limit(feature : Feature) -> Double {
  feature.nominal + feature.upper
}

///|
/// Return the normalized absolute deviation from nominal.
pub fn relative_error(feature : Feature, value : Double) -> Double {
  let width = tolerance_half_width(feature)
  if width == 0.0 {
    if value == feature.nominal {
      0.0
    } else {
      1.0
    }
  } else {
    (value - feature.nominal).abs() / width
  }
}

///|
/// Return whether a sample count can support an uncertainty calculation.
pub fn sample_count_is_valid(count : Int) -> Bool {
  count > 0
}

///|
/// Return the distance of a value from the nearest specification limit.
pub fn limit_margin(feature : Feature, value : Double) -> Double {
  let lower_margin = value - lower_limit(feature)
  let upper_margin = upper_limit(feature) - value
  if lower_margin < upper_margin {
    lower_margin
  } else {
    upper_margin
  }
}

///|
/// Return the signed deviation from a feature's nominal value.
pub fn signed_deviation(feature : Feature, value : Double) -> Double {
  value - feature.nominal
}

///|
/// Classify an uncertainty ratio into a planning risk level.
pub fn risk_for_uncertainty(
  uncertainty : Double,
  tolerance : Double,
) -> RiskLevel {
  if tolerance <= 0.0 {
    RiskLevel::High
  } else {
    let ratio = uncertainty / tolerance
    if ratio <= 0.25 {
      RiskLevel::Low
    } else if ratio <= 0.5 {
      RiskLevel::Medium
    } else {
      RiskLevel::High
    }
  }
}

///|
/// Build a per-feature result from an optional observation.
pub fn feature_result(
  feature~ : Feature,
  observation~ : Observation?,
) -> FeatureResult {
  match observation {
    None =>
      {
        feature_id: feature.id,
        verdict: Verdict::Unmeasured,
        deviation: 0.0,
        risk: RiskLevel::High,
        sample_count: 0,
      }
    Some(observation) => {
      let verdict = classify(feature, Some(observation))
      let deviation = relative_error(feature, observation.value)
      let risk = if verdict is Verdict::Pass {
        RiskLevel::Low
      } else {
        RiskLevel::High
      }
      {
        feature_id: feature.id,
        verdict,
        deviation,
        risk,
        sample_count: observation.sample_count,
      }
    }
  }
}