use crate::models::UtxoAgeVariance;
const LOG2_MAX_AGE: f64 = 20.516_f64;
pub fn encode_utxo_age_log(age_blocks: i64) -> u8 {
if age_blocks <= 0 {
return 0;
}
let log_val = (1.0 + age_blocks as f64).log2();
((255.0 * log_val / LOG2_MAX_AGE).round() as i32).clamp(0, 255) as u8
}
pub fn calculate_utxo_age_variance(ages_blocks: &[i64], blocks_per_day: u32) -> UtxoAgeVariance {
if ages_blocks.is_empty() {
return UtxoAgeVariance::Uniform;
}
let n = ages_blocks.len() as f64;
let mean = ages_blocks.iter().sum::<i64>() as f64 / n;
let variance = ages_blocks
.iter()
.map(|&age| {
let diff = age as f64 - mean;
diff * diff
})
.sum::<f64>()
/ n;
let variance_days = variance.sqrt() / blocks_per_day as f64;
if variance_days < 7.0 {
UtxoAgeVariance::Uniform
} else if variance_days < 90.0 {
UtxoAgeVariance::Mixed
} else if variance_days < 365.0 {
UtxoAgeVariance::Dispersed
} else {
UtxoAgeVariance::HighlyDispersed
}
}
pub fn analyze_utxo_ages(ages_blocks: &[i64], blocks_per_day: u32) -> (u8, u8, UtxoAgeVariance) {
if ages_blocks.is_empty() {
return (0u8, 0u8, UtxoAgeVariance::Uniform);
}
let mut sorted = ages_blocks.to_vec();
sorted.sort_unstable();
let max = sorted.last().copied().unwrap_or(0);
let median = if sorted.len() % 2 == 0 {
(sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2
} else {
sorted[sorted.len() / 2]
};
let max_tier = encode_utxo_age_log(max);
let median_tier = encode_utxo_age_log(median);
let variance = calculate_utxo_age_variance(&sorted, blocks_per_day);
(max_tier, median_tier, variance)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_zero_blocks() {
assert_eq!(encode_utxo_age_log(0), 0);
assert_eq!(encode_utxo_age_log(-1), 0);
}
#[test]
fn test_encode_key_thresholds() {
let day = encode_utxo_age_log(144);
assert!(
day >= 85 && day <= 93,
"1 day: expected tier ~89, obtained {day}"
);
let year = encode_utxo_age_log(52_560);
assert!(
year >= 191 && year <= 199,
"1 year: expected tier ~195, obtained {year}"
);
let twelve_years = encode_utxo_age_log(630_720);
assert!(
twelve_years >= 235 && twelve_years <= 243,
"12 years: expected tier ~239, obtained {twelve_years}"
);
}
#[test]
fn test_encode_monotonic() {
let t1 = encode_utxo_age_log(144);
let t2 = encode_utxo_age_log(1008);
let t3 = encode_utxo_age_log(52_560);
assert!(t1 < t2 && t2 < t3, "encoding must be strictly increasing");
}
#[test]
fn test_encode_max_clamp() {
let extreme = encode_utxo_age_log(2_000_000);
assert_eq!(
extreme, 255,
"values above the ceiling should be clamped to 255"
);
}
#[test]
fn test_analyze_utxo_ages_empty() {
let (max, med, var) = analyze_utxo_ages(&[], 144);
assert_eq!(max, 0);
assert_eq!(med, 0);
assert_eq!(var, UtxoAgeVariance::Uniform);
}
#[test]
fn test_analyze_utxo_ages_single() {
let ages = vec![52_560i64];
let (max, med, var) = analyze_utxo_ages(&ages, 144);
assert_eq!(max, med, "single element: max == median");
assert_eq!(var, UtxoAgeVariance::Uniform);
}
#[test]
fn test_analyze_variance_uniform() {
let ages = vec![100i64, 110, 120, 130];
let (_, _, var) = analyze_utxo_ages(&ages, 144);
assert_eq!(var, UtxoAgeVariance::Uniform);
}
#[test]
fn test_analyze_variance_highly_dispersed() {
let ages = vec![144i64, 262_800];
let (_, _, var) = analyze_utxo_ages(&ages, 144);
assert_eq!(var, UtxoAgeVariance::HighlyDispersed);
}
}