///|
pub fn bucket_events(request : Value) -> Value raise {
  let width = num(get(request, "width_seconds"))
  let origin = optional_num(get(request, "origin"), 0.0)
  if width <= 0.0 || width > 1000000000.0 {
    raise InputError("invalid time bucket width")
  }
  let report = analyze(request)
  let events = arr(get(report, "events"))
  let buckets : Map[String, Array[Value]] = Map([])
  let starts : Map[String, Double] = Map([])
  for event in events {
    let start = ((num(get(event, "timestamp")) - origin) / width).floor() *
      width +
      origin
    if start.is_nan() ||
      start.is_inf() ||
      (start + width).is_inf() ||
      start + width == start {
      raise InputError("time bucket precision or range exceeded")
    }
    let key = canonical(Number(start))
    let bucket = buckets.get(key).unwrap_or([])
    bucket.push(event)
    buckets[key] = bucket
    starts[key] = start
  }
  let keys = buckets.keys().collect()
  keys.sort_by(fn(a, b) { starts[a].compare(starts[b]) })
  let output = keys.map(fn(key) {
    record([
      ("start", Number(starts[key])),
      ("end", Number(starts[key] + width)),
      ("metrics", metric(buckets[key])),
    ])
  })
  record([
    ("buckets", Array(output)),
    ("width_seconds", Number(width)),
    ("origin", Number(origin)),
    ("events_considered", Number(events.length().to_double())),
    ("diagnostics", get(report, "diagnostics")),
    (
      "interval",
      String("start inclusive; end exclusive; empty buckets omitted"),
    ),
  ])
}