///|
pub fn batch_mean_by_name(
  batches : Array[BatchRun],
  name : String,
) -> StatsSummary {
  let values : Array[Double] = []
  for batch in batches {
    for measurement in batch.records {
      if measurement.name == name {
        values.push(measurement.value)
      }
    }
  }
  describe_samples(values)
}

///|
pub fn batch_decision_rate(batches : Array[BatchRun]) -> Double {
  let mut total = 0
  let mut accepted = 0
  for batch in batches {
    let summary = batch.summary()
    total += summary.total
    accepted += summary.pass + summary.warn
  }
  if total == 0 {
    0.0
  } else {
    Double::from_int(accepted) / Double::from_int(total)
  }
}

///|
pub fn batch_values_by_index(
  batches : Array[BatchRun],
  index : Int,
) -> Array[Double] {
  let result : Array[Double] = []
  for batch in batches {
    if index >= 0 && index < batch.records.length() {
      result.push(batch.records[index].value)
    }
  }
  result
}

///|
pub fn batch_control_limits(
  values : Array[Double],
  sigma : Double,
) -> DoubleBounds {
  let summary = describe_samples(values)
  let margin = sigma.abs() * summary.standard_deviation
  { min: summary.mean - margin, max: summary.mean + margin }
}

///|
pub fn batch_out_of_control(
  values : Array[Double],
  limits : DoubleBounds,
) -> Array[Bool] {
  values.map(fn(value) { value < limits.min || value > limits.max })
}

///|
pub fn batch_moving_average(
  values : Array[Double],
  window : Int,
) -> Array[Double] {
  moving_average(values, window)
}

///|
pub fn batch_cumulative_sum(values : Array[Double]) -> Array[Double] {
  let result : Array[Double] = []
  let mut total = 0.0
  for value in values {
    total += value
    result.push(total)
  }
  result
}

///|
pub fn batch_cumulative_mean(values : Array[Double]) -> Array[Double] {
  let result : Array[Double] = []
  let mut total = 0.0
  for i in 0.. Double {
  if values.length() < 2 {
    0.0
  } else {
    (values[values.length() - 1] - values[0]) /
    Double::from_int(values.length() - 1)
  }
}

///|
pub fn batch_linear_trend(values : Array[Double]) -> Double {
  if values.length() < 2 {
    0.0
  } else {
    let x : Array[Double] = []
    for i in 0.. Array[Array[Measurement]] {
  let pass : Array[Measurement] = []
  let warn : Array[Measurement] = []
  let fail : Array[Measurement] = []
  for measurement in measurements {
    match measurement.decision {
      Pass => pass.push(measurement)
      Warn => warn.push(measurement)
      Fail => fail.push(measurement)
    }
  }
  [pass, warn, fail]
}

///|
pub fn batch_measurement_table(batch : BatchRun) -> MeasurementTable {
  let rows : Array[Array[String]] = []
  for measurement in batch.records {
    rows.push([
      measurement.name,
      measurement.value.to_string(),
      measurement.unit,
      decision_name(measurement.decision),
      measurement.deviation.to_string(),
    ])
  }
  { columns: ["name", "value", "unit", "decision", "deviation"], rows }
}

///|
pub fn render_measurement_table(table : MeasurementTable) -> String {
  let out = StringBuilder()
  for column in table.columns {
    out.write_string(column)
    out.write_string("\t")
  }
  out.write_string("\n")
  for row in table.rows {
    for cell in row {
      out.write_string(cell)
      out.write_string("\t")
    }
    out.write_string("\n")
  }
  out.to_string()
}

///|
pub fn batch_snapshot(batch : BatchRun) -> String {
  let stats = batch_statistics(batch)
  batch.id +
  ":" +
  stats.total.to_string() +
  ":" +
  stats.accepted.to_string() +
  ":" +
  stats.rejected.to_string() +
  ":" +
  stats.mean.to_string()
}

///|
pub fn compare_batches(
  first : BatchRun,
  second : BatchRun,
) -> Array[Measurement] {
  measurement_delta(first.records, second.records)
}