Skip to main content

energyml_section/
inputs.rs

1//! Checking the inputs before anything is read (ADR-0015).
2//!
3//! Contract: [`validate`] and [`InputsError`].
4//!
5//! Every refusal names the field and the value it found: a width of `-3` is
6//! said as such, never as "invalid settings". The checks go one step past what
7//! the type says, to what the calculation downstream would refuse in its own
8//! words (`Corridor` wants a width on at least one side and no zero-length
9//! segment): refusing here keeps the message about *the inputs*.
10
11use crate::model::{CrossWidths, Crosses, Inputs};
12
13/// The most cross sections one set of inputs may ask for: a guard against a
14/// spacing of a millimetre on a trace of kilometres, not a limit anyone meets.
15pub const MAX_CROSSES: u64 = 10_000;
16
17/// Inputs that cannot make a catalogue - each refusal names what is wrong.
18#[derive(Debug, Clone, PartialEq, thiserror::Error)]
19pub enum InputsError {
20    /// The trace is not a trace (fewer than two distinct points, a coordinate
21    /// that is not a number…).
22    #[error("the trace {0}")]
23    Trace(String),
24    /// A width, spacing or count that is not usable.
25    #[error("{0}")]
26    Setting(String),
27}
28
29/// Whether these inputs can make a catalogue.
30pub fn validate(inputs: &Inputs) -> Result<(), InputsError> {
31    let trace = &inputs.trace;
32    if trace.len() < 2 {
33        return Err(InputsError::Trace(format!(
34            "needs at least two points, got {}",
35            trace.len()
36        )));
37    }
38    if let Some(i) = trace.iter().position(|p| !p[0].is_finite() || !p[1].is_finite()) {
39        return Err(InputsError::Trace(format!(
40            "point {i} has a coordinate that is not a finite number: [{}, {}]",
41            trace[i][0], trace[i][1]
42        )));
43    }
44    if let Some(i) = (1..trace.len()).find(|&i| trace[i] == trace[i - 1]) {
45        return Err(InputsError::Trace(format!(
46            "points {} and {i} coincide ([{}, {}]): a segment of no length has no direction, \
47             so no side",
48            i - 1,
49            trace[i][0],
50            trace[i][1]
51        )));
52    }
53    width("the left width", inputs.left)?;
54    width("the right width", inputs.right)?;
55    if inputs.left == 0.0 && inputs.right == 0.0 {
56        return Err(InputsError::Setting(
57            "the long section's left and right widths are both 0: its corridor would keep nothing"
58                .to_string(),
59        ));
60    }
61    if let Some(f) = inputs.time_factor
62        && (!f.is_finite() || f <= 0.0)
63    {
64        return Err(InputsError::Setting(format!(
65            "the time factor must be a finite number above 0, got {f}"
66        )));
67    }
68    match &inputs.crosses {
69        Crosses::None => {}
70        Crosses::Every { spacing, common, own } => {
71            if !spacing.is_finite() || *spacing <= 0.0 {
72                return Err(InputsError::Setting(format!(
73                    "the cross sections' spacing must be a finite number above 0, got {spacing}"
74                )));
75            }
76            widths("the common cross sections'", common)?;
77            for o in own {
78                widths(&format!("cross section {}'s", o.index), &o.widths)?;
79            }
80            distinct(own.iter().map(|o| o.index))?;
81        }
82        Crosses::Count { count, common, own } => {
83            if *count < 1 {
84                return Err(InputsError::Setting(format!(
85                    "the number of cross sections must be at least 1, got {count}"
86                )));
87            }
88            if u64::from(*count) > MAX_CROSSES {
89                return Err(InputsError::Setting(format!(
90                    "{count} cross sections is over the limit of {MAX_CROSSES}"
91                )));
92            }
93            widths("the common cross sections'", common)?;
94            for o in own {
95                widths(&format!("cross section {}'s", o.index), &o.widths)?;
96            }
97            distinct(own.iter().map(|o| o.index))?;
98        }
99    }
100    Ok(())
101}
102
103fn width(name: &str, value: f64) -> Result<(), InputsError> {
104    if value.is_finite() && value >= 0.0 {
105        Ok(())
106    } else {
107        Err(InputsError::Setting(format!(
108            "{name} must be a finite number, not negative, got {value}"
109        )))
110    }
111}
112
113fn widths(who: &str, w: &CrossWidths) -> Result<(), InputsError> {
114    width(&format!("{who} length before"), w.before)?;
115    width(&format!("{who} length after"), w.after)?;
116    width(&format!("{who} left width"), w.left)?;
117    width(&format!("{who} right width"), w.right)?;
118    if w.before + w.after <= 0.0 {
119        return Err(InputsError::Setting(format!(
120            "{who} lengths before and after are both 0: a cross section of no length has no \
121             direction"
122        )));
123    }
124    if w.left == 0.0 && w.right == 0.0 {
125        return Err(InputsError::Setting(format!(
126            "{who} left and right widths are both 0: the corridor would keep nothing"
127        )));
128    }
129    Ok(())
130}
131
132fn distinct(indices: impl Iterator<Item = u32>) -> Result<(), InputsError> {
133    let mut seen = std::collections::HashSet::new();
134    for i in indices {
135        if !seen.insert(i) {
136            return Err(InputsError::Setting(format!(
137                "cross section {i} is set apart twice"
138            )));
139        }
140    }
141    Ok(())
142}