1pub fn percentile_rank(cohort: &[f64], v: f64) -> f64 {
12 if !v.is_finite() {
13 return 0.0;
14 }
15 let mut n = 0usize;
16 let mut below = 0usize;
17 let mut equal = 0usize;
18 for &c in cohort {
19 if !c.is_finite() {
20 continue;
21 }
22 n += 1;
23 if c < v {
24 below += 1;
25 } else if c == v {
26 equal += 1;
27 }
28 }
29 if n == 0 {
30 return 0.0;
31 }
32 (below as f64 + 0.5 * equal as f64) / n as f64
33}
34
35pub const MIN_COHORT: usize = 5;
37
38pub fn pe_industry_pctile(pe: f64, cohort: &[f64]) -> Option<f64> {
41 if !pe.is_finite() || pe <= 0.0 {
42 return None;
43 }
44 let finite = cohort.iter().filter(|c| c.is_finite()).count();
45 if finite < MIN_COHORT {
46 return None;
47 }
48 Some(percentile_rank(cohort, pe) * 100.0)
49}
50
51pub fn analyst_upside_pct(target: f64, close: f64) -> Option<f64> {
53 if !target.is_finite() || !close.is_finite() || close <= 0.0 {
54 return None;
55 }
56 Some((target - close) / close * 100.0)
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn percentile_rank_midrank_and_edges() {
65 let cohort = [10.0, 20.0, 30.0, 40.0, 50.0];
66 assert!((percentile_rank(&cohort, 30.0) - 0.5).abs() < 1e-9);
67 assert_eq!(percentile_rank(&[], 1.0), 0.0);
68 assert_eq!(percentile_rank(&cohort, f64::NAN), 0.0);
69 }
70
71 #[test]
72 fn pe_pctile_scale_and_thin_cohort() {
73 let cohort = [10.0, 20.0, 30.0, 40.0, 50.0];
74 assert_eq!(pe_industry_pctile(30.0, &cohort), Some(50.0));
75 assert_eq!(pe_industry_pctile(30.0, &[10.0, 20.0]), None);
76 assert_eq!(pe_industry_pctile(-1.0, &cohort), None);
77 }
78
79 #[test]
80 fn upside() {
81 assert_eq!(analyst_upside_pct(120.0, 100.0), Some(20.0));
82 assert!(analyst_upside_pct(1.0, 0.0).is_none());
83 assert!(analyst_upside_pct(f64::NAN, 100.0).is_none());
84 }
85}