ballistics_engine/
adjustment.rs1#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum ClickBase {
14 Mil,
15 Moa,
16 Smoa,
17}
18
19pub fn adjustment_factor(base: ClickBase) -> f64 {
25 match base {
26 ClickBase::Mil => 1000.0,
27 ClickBase::Moa => 3438.0,
28 ClickBase::Smoa => 3600.0,
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct ClickValue {
36 pub size: f64,
37 pub base: ClickBase,
38}
39
40pub fn parse_click_value(s: &str) -> Result<ClickValue, String> {
44 let t = s.trim().to_ascii_lowercase();
45 let (num, base) = if let Some(n) = t.strip_suffix("smoa") {
46 (n, ClickBase::Smoa)
47 } else if let Some(n) = t.strip_suffix("iphy") {
48 (n, ClickBase::Smoa)
49 } else if let Some(n) = t.strip_suffix("moa") {
50 (n, ClickBase::Moa)
51 } else if let Some(n) = t.strip_suffix("mil") {
52 (n, ClickBase::Mil)
53 } else {
54 return Err(format!(
55 "click value '{s}' needs a unit suffix: mil, moa, smoa, or iphy (e.g. 0.1mil, 0.25moa)"
56 ));
57 };
58 let size: f64 = num
59 .trim()
60 .parse()
61 .map_err(|_| format!("click value '{s}' has an invalid number '{num}'"))?;
62 if !size.is_finite() || size <= 0.0 {
63 return Err(format!("click value '{s}' must be a positive, finite graduation"));
64 }
65 Ok(ClickValue { size, base })
66}
67
68pub fn clicks_for(drop_yd: f64, range_yd: f64, click: &ClickValue) -> i64 {
73 if range_yd < 1.0 {
74 return 0;
75 }
76 let angle = (drop_yd / range_yd) * adjustment_factor(click.base);
77 (angle / click.size).round() as i64
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn parse_click_value_accepts_suffixed_and_rejects_bare() {
86 let c = parse_click_value("0.25moa").unwrap();
87 assert!((c.size - 0.25).abs() < 1e-12);
88 assert!(matches!(c.base, ClickBase::Moa));
89 assert!(matches!(parse_click_value("0.1mil").unwrap().base, ClickBase::Mil));
90 assert!(matches!(parse_click_value("0.125smoa").unwrap().base, ClickBase::Smoa));
91 assert!(matches!(parse_click_value("0.125iphy").unwrap().base, ClickBase::Smoa));
92 for bad in ["0.25", "moa", "-0.1mil", "0mil", "0.1mils", ""] {
93 assert!(parse_click_value(bad).is_err(), "{bad:?} must be rejected");
94 }
95 let e = parse_click_value("0.25").unwrap_err();
97 assert!(e.contains("mil") && e.contains("moa"), "{e}");
98 }
99
100 #[test]
101 fn clicks_round_to_nearest_whole_click() {
102 let quarter_moa = parse_click_value("0.25moa").unwrap();
103 let drop_yd = 2.6 / 3438.0 * 100.0;
105 assert_eq!(clicks_for(drop_yd, 100.0, &quarter_moa), 10);
106 assert_eq!(clicks_for(-drop_yd, 100.0, &quarter_moa), -10);
108 let drop_yd = 2.625 / 3438.0 * 100.0;
110 assert_eq!(clicks_for(drop_yd, 100.0, &quarter_moa), 11);
111 }
112}