use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use num_bigint::BigInt;
#[must_use]
pub fn reliability_after_success(current: f64, increment: f64) -> f64 {
(current + increment).min(1.0)
}
#[must_use]
pub fn reliability_after_failure(current: f64, decay: f64, floor: f64) -> f64 {
(current * decay).max(floor)
}
#[must_use]
pub fn bigint_ratio(numerator: &BigInt, denominator: &BigInt) -> f64 {
if denominator == &BigInt::from(0u8) {
return 0.0;
}
let scale = BigInt::from(1_000_000_000u64);
let scaled = (numerator * &scale) / denominator;
bigint_to_f64(&scaled) / 1_000_000_000.0
}
#[must_use]
pub fn meets_quorum(accumulated: &BigInt, total: &BigInt, threshold: f64) -> bool {
bigint_ratio(accumulated, total) >= threshold
}
#[must_use]
pub fn weight_fraction(weight: &BigInt, total: &BigInt, count: usize) -> f64 {
if total == &BigInt::from(0u8) {
if count == 0 {
return 0.0;
}
return 1.0 / count as f64;
}
bigint_ratio(weight, total)
}
#[must_use]
pub fn selection_score(weight_fraction: f64, reliability: f64) -> f64 {
weight_fraction * reliability
}
#[must_use]
pub fn most_common_hash(hashes: &[String]) -> Option<String> {
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for hash in hashes {
*counts.entry(hash.as_str()).or_insert(0) += 1;
}
counts
.into_iter()
.max_by(|left, right| left.1.cmp(&right.1).then_with(|| right.0.cmp(left.0)))
.map(|(hash, _)| hash.to_string())
}
#[must_use]
pub fn overlapping_moment(windows: impl IntoIterator<Item = (i64, i64)>) -> Option<i64> {
let mut latest_from: Option<i64> = None;
let mut earliest_to: Option<i64> = None;
for (from, to) in windows {
latest_from = Some(latest_from.map_or(from, |seen: i64| seen.max(from)));
earliest_to = Some(earliest_to.map_or(to, |seen: i64| seen.min(to)));
}
match (latest_from, earliest_to) {
(Some(from), Some(to)) if from <= to => Some(from),
_ => None,
}
}
#[must_use]
pub fn next_backoff(delay: u64, max: u64) -> u64 {
delay.saturating_mul(2).min(max)
}
fn bigint_to_f64(value: &BigInt) -> f64 {
value.to_string().parse::<f64>().unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn success_adds_then_clamps_to_one() {
assert!((reliability_after_success(0.5, 0.1) - 0.6).abs() < 1e-9);
assert_eq!(reliability_after_success(0.95, 1.0), 1.0);
}
#[test]
fn failure_multiplies_then_respects_floor() {
assert!((reliability_after_failure(1.0, 0.5, 0.01) - 0.5).abs() < 1e-9);
assert_eq!(reliability_after_failure(0.5, 0.5, 0.4), 0.4);
}
#[test]
fn quorum_reached_at_or_above_threshold() {
assert!(meets_quorum(&BigInt::from(7), &BigInt::from(10), 0.7));
assert!(!meets_quorum(&BigInt::from(6), &BigInt::from(10), 0.7));
}
#[test]
fn quorum_never_reached_with_zero_total() {
assert!(!meets_quorum(&BigInt::from(5), &BigInt::from(0), 0.7));
}
#[test]
fn weight_fraction_splits_evenly_when_total_is_zero() {
assert!((weight_fraction(&BigInt::from(1), &BigInt::from(0), 4) - 0.25).abs() < 1e-9);
assert_eq!(weight_fraction(&BigInt::from(1), &BigInt::from(0), 0), 0.0);
}
#[test]
fn weight_fraction_is_the_ratio_when_total_is_nonzero() {
assert!((weight_fraction(&BigInt::from(3), &BigInt::from(4), 2) - 0.75).abs() < 1e-9);
}
#[test]
fn selection_score_scales_weight_by_reliability() {
assert!((selection_score(0.5, 0.4) - 0.2).abs() < 1e-9);
}
#[test]
fn most_common_hash_picks_the_majority() {
let hashes = vec!["a".to_string(), "b".to_string(), "a".to_string()];
assert_eq!(most_common_hash(&hashes).as_deref(), Some("a"));
}
#[test]
fn most_common_hash_breaks_ties_lexicographically() {
let hashes = vec!["b".to_string(), "a".to_string()];
assert_eq!(most_common_hash(&hashes).as_deref(), Some("a"));
}
#[test]
fn most_common_hash_is_none_without_observations() {
assert_eq!(most_common_hash(&[]), None);
}
#[test]
fn overlapping_moment_picks_latest_from() {
assert_eq!(overlapping_moment([(0, 100), (50, 200), (30, 80)]), Some(50));
}
#[test]
fn overlapping_moment_is_none_when_disjoint() {
assert_eq!(overlapping_moment([(0, 40), (50, 100)]), None);
}
#[test]
fn overlapping_moment_is_none_when_empty() {
assert_eq!(overlapping_moment(core::iter::empty::<(i64, i64)>()), None);
}
#[test]
fn backoff_doubles_until_capped() {
assert_eq!(next_backoff(1, 500), 2);
assert_eq!(next_backoff(2, 500), 4);
assert_eq!(next_backoff(256, 500), 500);
}
#[test]
fn backoff_saturates_without_overflow() {
assert_eq!(next_backoff(u64::MAX, 500), 500);
}
}