///|
/// Integer-only descriptive statistics for currency and count metrics.
pub struct NumericSummary {
count : Int
total : Int
minimum : Int
maximum : Int
mean : Int
median : Int
lower_quartile : Int
upper_quartile : Int
}
///|
pub struct ValueChange {
baseline : Int
current : Int
absolute : Int
percent : Int
direction : TrendDirection
}
///|
fn percentile_index(length : Int, percent : Int) -> Int {
if length <= 1 {
0
} else {
let bounded = if percent < 0 {
0
} else if percent > 100 {
100
} else {
percent
}
(length - 1) * bounded / 100
}
}
///|
/// Return a nearest-rank percentile without floating-point or hidden rounding.
pub fn percentile(values : Array[Int], percent : Int) -> Int {
if values.is_empty() {
0
} else {
let sorted = values.copy()
sorted.sort()
sorted[percentile_index(sorted.length(), percent)]
}
}
///|
/// Summarize a finite integer population with stable boundary behavior.
pub fn summarize_values(values : Array[Int]) -> NumericSummary {
if values.is_empty() {
return {
count: 0,
total: 0,
minimum: 0,
maximum: 0,
mean: 0,
median: 0,
lower_quartile: 0,
upper_quartile: 0,
}
}
let sorted = values.copy()
sorted.sort()
let total = values.fold(init=0, (sum, value) => sum + value)
{
count: sorted.length(),
total,
minimum: sorted[0],
maximum: sorted[sorted.length() - 1],
mean: total / sorted.length(),
median: sorted[percentile_index(sorted.length(), 50)],
lower_quartile: sorted[percentile_index(sorted.length(), 25)],
upper_quartile: sorted[percentile_index(sorted.length(), 75)],
}
}
///|
/// Compare two periods using integer arithmetic and an explicit zero baseline.
pub fn compare_values(baseline : Int, current : Int) -> ValueChange {
let absolute = current - baseline
{
baseline,
current,
absolute,
percent: if baseline == 0 {
0
} else {
absolute * 100 / baseline
},
direction: if absolute > 0 {
Rising
} else if absolute < 0 {
Falling
} else {
Flat
},
}
}
///|
/// Return values outside a robust interquartile fence.
pub fn iqr_outliers(values : Array[Int]) -> Array[Int] {
if values.length() < 4 {
return []
}
let q1 = percentile(values, 25)
let q3 = percentile(values, 75)
let spread = q3 - q1
let low = q1 - spread * 3 / 2
let high = q3 + spread * 3 / 2
values.filter(value => value < low || value > high)
}
///|
/// Compute a rolling mean for dashboard trend lines.
pub fn rolling_mean(values : Array[Int], window : Int) -> Array[Int] {
if window <= 0 {
return []
}
let result : Array[Int] = []
for index in 0..