///|
/// Fixed-width integer histogram for quick distribution previews.
pub(all) struct IntHistogram {
min : Int
max : Int
buckets : Int
counts : Array[Int]
underflow : Int
overflow : Int
total : Int
} derive(Eq, Debug)
///|
pub(all) struct HistogramBucket {
index : Int
start : Int
end : Int
count : Int
} derive(Eq, Debug)
///|
pub(all) struct HistogramStats {
total : Int
underflow : Int
overflow : Int
non_empty_buckets : Int
mode_bucket : Int
mode_count : Int
} derive(Eq, Debug)
///|
pub fn histogram_new(min : Int, max : Int, buckets : Int) -> IntHistogram {
let safe_buckets = sketch_max(1, buckets)
let safe_max = if max <= min { min + safe_buckets } else { max }
{
min,
max: safe_max,
buckets: safe_buckets,
counts: Array::make(safe_buckets, 0),
underflow: 0,
overflow: 0,
total: 0,
}
}
///|
pub fn histogram_add(histogram : IntHistogram, value : Int) -> IntHistogram {
let counts = histogram.counts
let mut underflow = histogram.underflow
let mut overflow = histogram.overflow
if value < histogram.min {
underflow += 1
} else if value > histogram.max {
overflow += 1
} else {
let index = histogram_index(histogram, value)
counts[index] += 1
}
{
min: histogram.min,
max: histogram.max,
buckets: histogram.buckets,
counts,
underflow,
overflow,
total: histogram.total + 1,
}
}
///|
pub fn histogram_from_values(
values : Array[Int],
min : Int,
max : Int,
buckets : Int,
) -> IntHistogram {
let mut histogram = histogram_new(min, max, buckets)
for value in values {
histogram = histogram_add(histogram, value)
}
histogram
}
///|
pub fn histogram_from_text(
input : String,
min : Int,
max : Int,
buckets : Int,
) -> IntHistogram {
let values : Array[Int] = Array::new()
for token in parse_events(input) {
match histogram_parse_int(token) {
Some(value) => values.push(value)
None => ()
}
}
histogram_from_values(values, min, max, buckets)
}
///|
pub fn histogram_bucket_width(histogram : IntHistogram) -> Int {
let span = histogram.max - histogram.min + 1
let width = span / histogram.buckets
if width <= 0 {
1
} else {
width
}
}
///|
pub fn histogram_index(histogram : IntHistogram, value : Int) -> Int {
let width = histogram_bucket_width(histogram)
let raw = (value - histogram.min) / width
if raw < 0 {
0
} else if raw >= histogram.buckets {
histogram.buckets - 1
} else {
raw
}
}
///|
pub fn histogram_buckets(histogram : IntHistogram) -> Array[HistogramBucket] {
let width = histogram_bucket_width(histogram)
let out : Array[HistogramBucket] = Array::new()
for i in 0.. HistogramStats {
let mut non_empty = 0
let mut mode_bucket = 0
let mut mode_count = 0
for i in 0.. 0 {
non_empty += 1
}
if count > mode_count {
mode_bucket = i
mode_count = count
}
}
{
total: histogram.total,
underflow: histogram.underflow,
overflow: histogram.overflow,
non_empty_buckets: non_empty,
mode_bucket,
mode_count,
}
}
///|
pub fn histogram_approx_percentile(
histogram : IntHistogram,
percentile : Int,
) -> Int? {
let in_range = histogram.total - histogram.underflow - histogram.overflow
if in_range <= 0 {
return None
}
let pct = if percentile < 0 {
0
} else if percentile > 100 {
100
} else {
percentile
}
let target = sketch_max(1, (in_range * pct + 99) / 100)
let mut seen = 0
for bucket in histogram_buckets(histogram) {
seen += bucket.count
if seen >= target {
return Some((bucket.start + bucket.end) / 2)
}
}
Some(histogram.max)
}
///|
pub fn histogram_markdown(histogram : IntHistogram) -> String {
let stats = histogram_stats(histogram)
let out = StringBuilder()
out.write_string("# Histogram Report\n\n")
out.write_string("| metric | value |\n| --- | ---: |\n")
out.write_string("| total | " + stats.total.to_string() + " |\n")
out.write_string("| underflow | " + stats.underflow.to_string() + " |\n")
out.write_string("| overflow | " + stats.overflow.to_string() + " |\n")
out.write_string(
"| non-empty buckets | " + stats.non_empty_buckets.to_string() + " |\n",
)
out.write_string("| mode bucket | " + stats.mode_bucket.to_string() + " |\n")
out.write_string("| mode count | " + stats.mode_count.to_string() + " |\n\n")
out.write_string(
"| bucket | range | count | bar |\n| ---: | --- | ---: | --- |\n",
)
for bucket in histogram_buckets(histogram) {
out.write_string(
"| " +
bucket.index.to_string() +
" | " +
bucket.start.to_string() +
".." +
bucket.end.to_string() +
" | " +
bucket.count.to_string() +
" | " +
histogram_bar(bucket.count, stats.mode_count) +
" |\n",
)
}
out.to_string()
}
///|
pub fn histogram_json(histogram : IntHistogram) -> String {
let stats = histogram_stats(histogram)
let out = StringBuilder()
out.write_string("{")
out.write_string("\"total\":" + stats.total.to_string() + ",")
out.write_string("\"underflow\":" + stats.underflow.to_string() + ",")
out.write_string("\"overflow\":" + stats.overflow.to_string() + ",")
out.write_string("\"buckets\":[")
let buckets = histogram_buckets(histogram)
for i in 0.. 0 {
out.write_string(",")
}
let bucket = buckets[i]
out.write_string(
"{\"index\":" +
bucket.index.to_string() +
",\"start\":" +
bucket.start.to_string() +
",\"end\":" +
bucket.end.to_string() +
",\"count\":" +
bucket.count.to_string() +
"}",
)
}
out.write_string("]}")
out.to_string()
}
///|
fn histogram_bar(count : Int, max_count : Int) -> String {
if count <= 0 || max_count <= 0 {
return ""
}
let width = sketch_max(1, (count * 12 + max_count - 1) / max_count)
let out = StringBuilder()
for _ in 0.. Int? {
try @string.parse_int(token) catch {
_ => None
} noraise {
value => Some(value)
}
}