///|
/// A closed time interval used by batch and streaming detectors.
pub(all) struct TimeWindow {
  start : Int
  end : Int
} derive(Eq, Debug)

///|
pub fn TimeWindow::new(start : Int, duration : Int) -> TimeWindow {
  let safe_duration = if duration < 0 { 0 } else { duration }
  { start, end: start + safe_duration }
}

///|
pub fn TimeWindow::contains(self : TimeWindow, timestamp : Int) -> Bool {
  timestamp >= self.start && timestamp <= self.end
}

///|
pub fn TimeWindow::duration(self : TimeWindow) -> Int {
  self.end - self.start
}

///|
pub fn TimeWindow::overlaps(self : TimeWindow, other : TimeWindow) -> Bool {
  self.start <= other.end && other.start <= self.end
}

///|
pub fn TimeWindow::intersection(
  self : TimeWindow,
  other : TimeWindow,
) -> TimeWindow? {
  if self.overlaps(other) {
    let start = if self.start > other.start { self.start } else { other.start }
    let end = if self.end < other.end { self.end } else { other.end }
    Some({ start, end })
  } else {
    None
  }
}

///|
pub fn transactions_in_window(
  transactions : Array[Transaction],
  window : TimeWindow,
) -> Array[Transaction] {
  let result : Array[Transaction] = []
  for tx in transactions {
    if window.contains(tx.occurred_at) {
      result.push(tx)
    }
  }
  result
}

///|
pub fn transactions_for_customer(
  transactions : Array[Transaction],
  customer : String,
) -> Array[Transaction] {
  let result : Array[Transaction] = []
  for tx in transactions {
    if tx.customer_id == customer {
      result.push(tx)
    }
  }
  result
}

///|
pub fn transactions_for_account(
  transactions : Array[Transaction],
  account : String,
) -> Array[Transaction] {
  let result : Array[Transaction] = []
  for tx in transactions {
    if tx.account_id == account {
      result.push(tx)
    }
  }
  result
}

///|
pub fn sort_by_time(transactions : Array[Transaction]) -> Array[Transaction] {
  let result : Array[Transaction] = []
  for tx in transactions {
    let mut inserted = false
    let mut index = 0
    for old in result {
      if !inserted && tx.occurred_at < old.occurred_at {
        result.insert(index, tx)
        inserted = true
      }
      index += 1
    }
    if !inserted {
      result.push(tx)
    }
  }
  result
}

///|
pub fn time_gaps(transactions : Array[Transaction]) -> Array[Int] {
  let sorted = sort_by_time(transactions)
  let gaps : Array[Int] = []
  if sorted.length() > 1 {
    for i in 1..