Skip to main content

_diffctx/config/
env_overrides.rs

1//! Shared helpers for reading config parameters from environment variables.
2//!
3//! Used by `category_weights.rs` and the Group-C operational-parameter
4//! overrides documented in `docs/parameter-strategy.md`. The pattern is:
5//! `Lazy::new` reads the env var once at first access; tests verify the
6//! pure parser (`parse_*_or_default`) directly so they do not need to
7//! mutate process-global env state.
8
9pub fn parse_f64_or_default(raw: Option<String>, default: f64) -> f64 {
10    raw.and_then(|s| s.parse::<f64>().ok())
11        .filter(|v| v.is_finite() && *v >= 0.0)
12        .unwrap_or(default)
13}
14
15pub fn parse_fraction_or_default(raw: Option<String>, default: f64) -> f64 {
16    parse_f64_or_default(raw, default).clamp(0.0, 1.0)
17}
18
19/// Parse a fraction strictly inside the open interval (0, 1).
20/// Used for parameters where 0.0 or 1.0 produce algorithmic degeneracy
21/// (e.g. PPR_ALPHA=1.0 makes restart probability zero, yielding all-zero rankings).
22pub fn parse_open_fraction_or_default(raw: Option<String>, default: f64) -> f64 {
23    const EPS: f64 = 1e-4;
24    parse_f64_or_default(raw, default).clamp(EPS, 1.0 - EPS)
25}
26
27pub fn parse_usize_or_default(raw: Option<String>, default: usize) -> usize {
28    raw.and_then(|s| s.parse::<usize>().ok()).unwrap_or(default)
29}
30
31pub fn parse_u32_or_default(raw: Option<String>, default: u32) -> u32 {
32    raw.and_then(|s| s.parse::<u32>().ok()).unwrap_or(default)
33}
34
35pub fn read_env_f64(name: &str, default: f64) -> f64 {
36    parse_f64_or_default(std::env::var(name).ok(), default)
37}
38
39pub fn read_env_fraction(name: &str, default: f64) -> f64 {
40    parse_fraction_or_default(std::env::var(name).ok(), default)
41}
42
43pub fn read_env_open_fraction(name: &str, default: f64) -> f64 {
44    parse_open_fraction_or_default(std::env::var(name).ok(), default)
45}
46
47pub fn read_env_usize(name: &str, default: usize) -> usize {
48    parse_usize_or_default(std::env::var(name).ok(), default)
49}
50
51pub fn read_env_u32(name: &str, default: u32) -> u32 {
52    parse_u32_or_default(std::env::var(name).ok(), default)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn f64_accepts_finite_nonneg() {
61        assert_eq!(parse_f64_or_default(Some("0.42".into()), 1.0), 0.42);
62        assert_eq!(parse_f64_or_default(Some("0".into()), 1.0), 0.0);
63    }
64
65    #[test]
66    fn f64_rejects_negative_and_nonfinite() {
67        assert_eq!(parse_f64_or_default(Some("-0.5".into()), 1.0), 1.0);
68        assert_eq!(parse_f64_or_default(Some("nan".into()), 1.0), 1.0);
69        assert_eq!(parse_f64_or_default(Some("inf".into()), 1.0), 1.0);
70    }
71
72    #[test]
73    fn fraction_clamps_into_unit_interval() {
74        assert_eq!(parse_fraction_or_default(Some("0.5".into()), 0.7), 0.5);
75        assert_eq!(parse_fraction_or_default(Some("1.05".into()), 0.7), 1.0);
76        assert_eq!(parse_fraction_or_default(Some("42".into()), 0.7), 1.0);
77        assert_eq!(parse_fraction_or_default(Some("-0.5".into()), 0.7), 0.7);
78        assert_eq!(parse_fraction_or_default(Some("nan".into()), 0.7), 0.7);
79        assert_eq!(parse_fraction_or_default(None, 0.7), 0.7);
80    }
81
82    #[test]
83    fn open_fraction_clamps_to_open_interval() {
84        // Boundary 1.0 → degenerate (PPR α=1 zeros all rankings); must clamp.
85        let v_one = parse_open_fraction_or_default(Some("1.0".into()), 0.6);
86        assert!(
87            v_one < 1.0,
88            "open fraction must clamp 1.0 below 1; got {v_one}"
89        );
90        assert!(
91            v_one > 0.99,
92            "clamp must stay near 1.0, not collapse to default"
93        );
94        // Boundary 0.0 → also degenerate; must clamp above 0.
95        let v_zero = parse_open_fraction_or_default(Some("0.0".into()), 0.6);
96        assert!(
97            v_zero > 0.0,
98            "open fraction must clamp 0.0 above 0; got {v_zero}"
99        );
100        // Interior values pass through.
101        assert_eq!(parse_open_fraction_or_default(Some("0.6".into()), 0.0), 0.6);
102        // Above 1.0 also clamped.
103        assert!(parse_open_fraction_or_default(Some("42".into()), 0.6) < 1.0);
104    }
105
106    #[test]
107    fn f64_falls_back_on_missing_or_unparseable() {
108        assert_eq!(parse_f64_or_default(None, 0.7), 0.7);
109        assert_eq!(parse_f64_or_default(Some("hello".into()), 0.7), 0.7);
110        assert_eq!(parse_f64_or_default(Some("".into()), 0.7), 0.7);
111    }
112
113    #[test]
114    fn usize_parses_or_falls_back() {
115        assert_eq!(parse_usize_or_default(Some("42".into()), 7), 42);
116        assert_eq!(parse_usize_or_default(Some("-1".into()), 7), 7);
117        assert_eq!(parse_usize_or_default(None, 7), 7);
118    }
119
120    #[test]
121    fn u32_parses_or_falls_back() {
122        assert_eq!(parse_u32_or_default(Some("24".into()), 8), 24);
123        assert_eq!(parse_u32_or_default(Some("nope".into()), 8), 8);
124    }
125}