///|
/// A nominal value with explicit lower and upper specification limits.
pub struct Specification {
  lower : Double
  upper : Double
  nominal : Double
} derive(Debug, Eq)

///|
/// Construct a specification band.
pub fn Specification::new(
  lower : Double,
  upper : Double,
  nominal : Double,
) -> Specification {
  if upper < lower {
    abort("specification upper bound must not be below lower bound")
  }
  if nominal < lower || nominal > upper {
    abort("specification nominal must lie within its limits")
  }
  { lower, upper, nominal }
}

///|
/// Return the specification as an acceptance window.
pub fn Specification::window(self : Specification) -> AcceptanceWindow {
  AcceptanceWindow::new(self.lower, self.upper)
}

///|
/// Return whether a value is within specification.
pub fn Specification::contains(self : Specification, value : Double) -> Bool {
  value >= self.lower && value <= self.upper
}

///|
/// Return the signed margin to the nearest limit.
pub fn Specification::margin(self : Specification, value : Double) -> Double {
  let lower = value - self.lower
  let upper = self.upper - value
  if lower < upper {
    lower
  } else {
    upper
  }
}

///|
/// Classification of one measurement against a specification band.
pub enum SpecificationStatus {
  BelowLower
  WithinSpecification
  AboveUpper
} derive(Debug, Eq)

///|
/// Classify a measurement against both limits.
pub fn Specification::classify(
  self : Specification,
  value : Double,
) -> SpecificationStatus {
  if value < self.lower {
    BelowLower
  } else if value > self.upper {
    AboveUpper
  } else {
    WithinSpecification
  }
}

///|
/// Aggregated specification results for an inspection batch.
pub struct SpecificationReport {
  count : Int
  accepted : Int
  lower_failures : Int
  upper_failures : Int
  yield_rate : Double
  mean_margin : Double
} derive(Debug, Eq)

///|
/// Evaluate every value and preserve lower/upper defect directions.
pub fn evaluate_specification(
  values : Array[Double],
  specification : Specification,
) -> SpecificationReport {
  if values.length() == 0 {
    abort("cannot evaluate an empty specification batch")
  }
  let mut accepted = 0
  let mut lower_failures = 0
  let mut upper_failures = 0
  let mut margin_total = 0.0
  for value in values {
    margin_total += specification.margin(value)
    match specification.classify(value) {
      BelowLower => lower_failures += 1
      WithinSpecification => accepted += 1
      AboveUpper => upper_failures += 1
    }
  }
  {
    count: values.length(),
    accepted,
    lower_failures,
    upper_failures,
    yield_rate: accepted.to_double() / values.length().to_double(),
    mean_margin: margin_total / values.length().to_double(),
  }
}