///|
fn observation_values(observations : Array[Observation]) -> Array[Double] {
  let values = []
  for observation in observations {
    values.push(observation.value)
  }
  values
}

///|
/// Summarize observed values and verdicts for one feature.
pub fn summarize_batch(
  feature~ : Feature,
  observations~ : Array[Observation],
) -> BatchSummary {
  let values = observation_values(observations)
  let mut pass_count = 0
  let mut fail_count = 0
  let mut unmeasured_count = 0
  for observation in observations {
    match classify(feature, Some(observation)) {
      Pass => pass_count += 1
      Fail(_) => fail_count += 1
      Unmeasured => unmeasured_count += 1
    }
  }
  let count = observations.length()
  let defect_rate = if count == 0 {
    0.0
  } else {
    fail_count.to_double() / count.to_double()
  }
  {
    feature_id: feature.id,
    count,
    pass_count,
    fail_count,
    unmeasured_count,
    mean: mean(values),
    minimum: minimum(values),
    maximum: maximum(values),
    range: range(values),
    standard_deviation: sample_stddev(values),
    defect_rate,
    capability: capability_indices(feature~, values~, uncertainty=0.0),
  }
}

///|
/// Return the pass rate for a summary.
pub fn pass_rate(summary : BatchSummary) -> Double {
  if summary.count == 0 {
    0.0
  } else {
    summary.pass_count.to_double() / summary.count.to_double()
  }
}

///|
/// Return whether all observed values passed.
pub fn batch_is_clean(summary : BatchSummary) -> Bool {
  summary.count > 0 && summary.fail_count == 0 && summary.unmeasured_count == 0
}

///|
/// Return a simple trend label based on the sequence of observed values.
pub fn batch_trend(values : Array[Double]) -> String {
  if values.length() < 2 {
    "stable"
  } else if is_non_decreasing(values) && values[values.length() - 1] > values[0] {
    "increasing"
  } else if is_non_increasing(values) && values[values.length() - 1] < values[0] {
    "decreasing"
  } else {
    "mixed"
  }
}