use std::collections::HashMap;
use url::Url;
fn wilson(hits: u32, n: u32) -> (f64, f64) {
pagesift_stat::beta::wilson_z(f64::from(hits), f64::from(n), *Z)
}
static Z: std::sync::LazyLock<f64> = std::sync::LazyLock::new(|| {
pagesift_stat::special::normal_quantile(1.0 - pagesift_stat::Delta::DEFAULT.get() / 2.0)
});
#[derive(Default)]
pub struct Harvest {
parts: HashMap<String, (u32, u32)>,
total: (u32, u32),
}
impl Harvest {
pub fn observe(&mut self, part: String, hit: bool) {
let p = self.parts.entry(part).or_default();
p.0 += 1;
p.1 += u32::from(hit);
self.total.0 += 1;
self.total.1 += u32::from(hit);
}
fn split(&self, part: &str) -> ((u32, u32), (u32, u32)) {
let mine = self.parts.get(part).copied().unwrap_or_default();
(mine, (self.total.0 - mine.0, self.total.1 - mine.1))
}
pub fn is_dry(&self, part: &str) -> bool {
let ((n, h), (rn, rh)) = self.split(part);
n > 0 && wilson(h, n).1 < wilson(rh, rn).0
}
pub fn is_proven(&self, part: &str) -> bool {
let ((n, h), (rn, rh)) = self.split(part);
n > 0 && wilson(h, n).0 > wilson(rh, rn).0
}
}
pub fn subtree(seed_host: &str, url: &Url) -> String {
let host = url
.host_str()
.unwrap_or_default()
.trim_start_matches("www.")
.to_ascii_lowercase();
if host != seed_host {
return host;
}
let mut segs: Vec<&str> = url.path().split('/').filter(|s| !s.is_empty()).collect();
segs.pop();
if segs.first().is_some_and(|s| {
super::url_shape::is_language_code(s) || super::url_shape::is_language_name(s)
}) {
segs.remove(0);
}
segs.truncate(2);
format!("{host}/{}", segs.join("/"))
}
#[cfg(test)]
mod tests {
use super::*;
fn u(s: &str) -> Url {
Url::parse(s).unwrap()
}
#[test]
fn a_part_that_never_pays_off_is_dropped_once_the_evidence_is_clear() {
let mut h = Harvest::default();
for i in 0..10 {
h.observe("lib".into(), i % 2 == 0);
}
for _ in 0..3 {
h.observe("journals".into(), false);
}
assert!(!h.is_dry("journals"));
for _ in 0..12 {
h.observe("journals".into(), false);
}
assert!(h.is_dry("journals"));
assert!(!h.is_dry("lib"));
assert!(h.is_proven("lib"));
assert!(!h.is_proven("journals"));
let mut h = Harvest::default();
for i in 0..40 {
h.observe("study".into(), i % 5 == 0);
h.observe("other".into(), i % 4 == 0);
}
assert!(!h.is_dry("study") && !h.is_dry("other"));
}
#[test]
fn wilson_uses_two_sided_delta() {
let old = |h: f64, n: f64| {
let z: f64 = 1.96;
let p = h / n;
let z2 = z * z;
let c = (p + z2 / (2.0 * n)) / (1.0 + z2 / n);
let half = z * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt() / (1.0 + z2 / n);
((c - half).max(0.0), (c + half).min(1.0))
};
for (h, n) in [(0, 1), (3, 4), (5, 40), (40, 40)] {
let (a, b) = wilson(h, n);
let (c, d) = old(f64::from(h), f64::from(n));
assert!((a - c).abs() < 1e-4 && (b - d).abs() < 1e-4, "{h}/{n}");
}
assert_eq!(wilson(0, 0), (0.0, 1.0));
}
#[test]
fn subtrees_follow_parent_directories() {
let seed = "e.edu";
assert_eq!(
subtree(seed, &u("https://journals.e.edu/j1/issue/current")),
"journals.e.edu"
);
assert_eq!(
subtree(seed, &u("https://www.e.edu/en/research-highlight/story-1")),
"e.edu/research-highlight"
);
assert_eq!(
subtree(seed, &u("https://e.edu/en/education/programmes/x/y")),
"e.edu/education/programmes"
);
assert_eq!(subtree(seed, &u("https://e.edu/about")), "e.edu/");
}
}