Skip to main content

hpr_motor/
delay.rs

1//! Ejection delays: the time from burnout to the ejection charge, or none for a plugged motor.
2//!
3//! Motor files and catalogs write a motor's available delays as one string. The RASP spec gives
4//! "delays separated by dashes", with `0` for an ejection charge with no delay and `P` for plugged
5//! (`docs/format/eng.md`). Real files also use commas, a trailing `P`, and `100` or `1000` as
6//! plugged markers; checked against ThrustCurve.org's metadata, `100` and `1000` nearly always mean
7//! plugged, and so does `0` more often than not (counts in `docs/format/eng.md` and `rse.md`). So
8//! this reader:
9//!
10//! - splits on `-` and `,`, dropping empty pieces with a warning;
11//! - reads `P` (either case), `100` and `1000` as [`Delay::Plugged`];
12//! - reads `0` as [`Delay::ZeroOrPlugged`], with a warning, so that it can't become an ejection at
13//!   burnout without a decision;
14//! - reads other numbers as [`Delay::Seconds`].
15//!
16//! The raw string stays in the file model so writers reproduce it exactly; physics should prefer
17//! the catalog's delays.
18
19use serde::{Deserialize, Serialize};
20
21use crate::text::WarningKind;
22
23/// One available delay setting. Serialized with a `kind` tag and the seconds as `value`:
24/// `{"kind":"seconds","value":6.0}`, `{"kind":"plugged"}`.
25// Read back by hand, from exactly those objects ([`DelayVisitor`]).
26#[derive(Debug, Clone, Copy, PartialEq, Serialize, schemars::JsonSchema)]
27#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
28#[non_exhaustive]
29#[serde(deny_unknown_fields)]
30pub enum Delay {
31    /// The ejection charge fires this many seconds after burnout: positive, or zero where a file
32    /// says so plainly, as a `.ork` does for a charge at burnout (`hpr_io::ork`).
33    Seconds(f64),
34    /// No ejection charge: the forward closure is plugged.
35    Plugged,
36    /// A `0`: the RASP spec means an ejection charge at burnout, but most files mean plugged
37    /// (`docs/format/eng.md`). The user or the catalog has to settle which.
38    ZeroOrPlugged,
39}
40
41impl<'de> Deserialize<'de> for Delay {
42    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
43        deserializer.deserialize_map(DelayVisitor)
44    }
45}
46
47/// Reads a [`Delay`] from exactly the objects its JSON Schema takes: a `value` beside `plugged` or
48/// `zero_or_plugged`, even `null`, is refused, as is a `[kind, value]` array. serde's derived
49/// reader for an adjacently tagged enum takes both.
50struct DelayVisitor;
51
52impl<'de> serde::de::Visitor<'de> for DelayVisitor {
53    type Value = Delay;
54
55    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
56        formatter.write_str("a delay: an object with a \"kind\", and a \"value\" for \"seconds\"")
57    }
58
59    fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Delay, A::Error> {
60        use serde::de::Error;
61        #[derive(Deserialize)]
62        #[serde(rename_all = "snake_case")]
63        enum Key {
64            Kind,
65            Value,
66        }
67        #[derive(Deserialize)]
68        #[serde(rename_all = "snake_case")]
69        enum Kind {
70            Seconds,
71            Plugged,
72            ZeroOrPlugged,
73        }
74        // The value can come before the kind, so it is read as a number or `null` and judged once
75        // both are in: `Some(None)` is a `"value": null`.
76        let (mut kind, mut value): (Option<Kind>, Option<Option<f64>>) = (None, None);
77        while let Some(key) = map.next_key::<Key>()? {
78            match key {
79                Key::Kind if kind.is_some() => return Err(A::Error::duplicate_field("kind")),
80                Key::Kind => kind = Some(map.next_value()?),
81                Key::Value if value.is_some() => return Err(A::Error::duplicate_field("value")),
82                Key::Value => value = Some(map.next_value()?),
83            }
84        }
85        match (kind, value) {
86            (None, _) => Err(A::Error::missing_field("kind")),
87            (Some(Kind::Seconds), Some(Some(seconds))) => Ok(Delay::Seconds(seconds)),
88            (Some(Kind::Seconds), Some(None)) => Err(A::Error::invalid_type(
89                serde::de::Unexpected::Unit,
90                &"the seconds, a number",
91            )),
92            (Some(Kind::Seconds), None) => Err(A::Error::missing_field("value")),
93            (Some(Kind::Plugged | Kind::ZeroOrPlugged), Some(_)) => Err(A::Error::custom(
94                "a \"value\" is given, which only a \"seconds\" delay has",
95            )),
96            (Some(Kind::Plugged), None) => Ok(Delay::Plugged),
97            (Some(Kind::ZeroOrPlugged), None) => Ok(Delay::ZeroOrPlugged),
98        }
99    }
100}
101
102/// A delay string read into settings.
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct DelayList {
105    /// The settings in the order written (some files list them longest first).
106    pub delays: Vec<Delay>,
107    /// Problems found while reading: an empty or unreadable piece ([`WarningKind::Dropped`], and
108    /// left out of `delays`), or an ambiguous `0` ([`WarningKind::Unusual`], kept as
109    /// [`Delay::ZeroOrPlugged`]).
110    pub warnings: Vec<DelayWarning>,
111}
112
113/// A problem found while reading a delay string.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct DelayWarning {
116    /// Whether a piece was dropped.
117    pub kind: WarningKind,
118    /// What was found.
119    pub message: String,
120}
121
122impl DelayList {
123    /// Reads a delay string such as `6-10-14`, `5,8,11`, `P`, `6-10-14-P` or `1000`. Any number
124    /// equal to 100 or 1000 (`1000.` too) is a plugged marker.
125    pub fn parse(raw: &str) -> Self {
126        let mut delays = Vec::new();
127        let mut warnings = Vec::new();
128        let trimmed = raw.trim();
129        if trimmed.is_empty() {
130            return Self { delays, warnings };
131        }
132        for piece in trimmed.split(['-', ',']) {
133            let piece = piece.trim();
134            match piece {
135                "" => warnings.push(DelayWarning {
136                    kind: WarningKind::Dropped,
137                    message: format!("empty delay in {raw:?} ignored"),
138                }),
139                "P" | "p" => delays.push(Delay::Plugged),
140                _ => match piece.parse::<f64>() {
141                    Ok(marker) if marker == 100.0 || marker == 1000.0 => {
142                        delays.push(Delay::Plugged)
143                    }
144                    Ok(0.0) => {
145                        warnings.push(DelayWarning {
146                            kind: WarningKind::Unusual,
147                            message: format!(
148                                "delay 0 in {raw:?} is ambiguous: the RASP spec means an ejection \
149                                 charge with no delay, but files mostly mean plugged"
150                            ),
151                        });
152                        delays.push(Delay::ZeroOrPlugged);
153                    }
154                    Ok(seconds) if seconds.is_finite() && seconds > 0.0 => {
155                        delays.push(Delay::Seconds(seconds));
156                    }
157                    _ => warnings.push(DelayWarning {
158                        kind: WarningKind::Dropped,
159                        message: format!("unreadable delay {piece:?} in {raw:?} ignored"),
160                    }),
161                },
162            }
163        }
164        Self { delays, warnings }
165    }
166
167    /// Whether any setting is plugged ([`Delay::Plugged`]; an ambiguous `0` doesn't count).
168    pub fn has_plugged(&self) -> bool {
169        self.delays.contains(&Delay::Plugged)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn plain_lists_and_markers() {
179        let list = DelayList::parse("6-10-14");
180        assert_eq!(
181            list.delays,
182            [
183                Delay::Seconds(6.0),
184                Delay::Seconds(10.0),
185                Delay::Seconds(14.0)
186            ]
187        );
188        assert!(list.warnings.is_empty());
189        assert_eq!(DelayList::parse("P").delays, [Delay::Plugged]);
190        assert_eq!(DelayList::parse("1000.").delays, [Delay::Plugged]);
191        assert_eq!(DelayList::parse("100.0").delays, [Delay::Plugged]);
192        assert_eq!(DelayList::parse(" p ").delays, [Delay::Plugged]);
193        assert_eq!(
194            DelayList::parse("10,14,1000").delays,
195            [Delay::Seconds(10.0), Delay::Seconds(14.0), Delay::Plugged]
196        );
197        assert!(DelayList::parse("").delays.is_empty());
198        assert!(!DelayList::parse("6").has_plugged());
199    }
200
201    #[test]
202    fn serde_form_is_tagged() {
203        let delays = [Delay::Seconds(6.0), Delay::Plugged, Delay::ZeroOrPlugged];
204        let json = serde_json::to_string(&delays).unwrap();
205        assert_eq!(
206            json,
207            r#"[{"kind":"seconds","value":6.0},{"kind":"plugged"},{"kind":"zero_or_plugged"}]"#
208        );
209        assert_eq!(serde_json::from_str::<Vec<Delay>>(&json).unwrap(), delays);
210        // The value may come first.
211        assert_eq!(
212            serde_json::from_str::<Delay>(r#"{"value":4.5,"kind":"seconds"}"#).unwrap(),
213            Delay::Seconds(4.5)
214        );
215    }
216
217    /// Only the objects the JSON Schema takes are read (issue #253): serde's derived reader took
218    /// `"value": null` beside a unit kind, and a `[kind, value]` array.
219    #[test]
220    fn serde_form_is_read_as_the_schema_says() {
221        let refused = |json: &str| serde_json::from_str::<Delay>(json).unwrap_err().to_string();
222        for json in [
223            r#"{"kind":"plugged","value":null}"#,
224            r#"{"kind":"zero_or_plugged","value":null}"#,
225            r#"{"value":3.0,"kind":"plugged"}"#,
226        ] {
227            assert!(
228                refused(json).contains("which only a \"seconds\" delay has"),
229                "{json}: {}",
230                refused(json)
231            );
232        }
233        assert!(refused(r#"{"kind":"seconds","value":null}"#).contains("the seconds, a number"));
234        assert!(refused(r#"{"kind":"seconds"}"#).contains("missing field `value`"));
235        assert!(refused(r#"{"value":3.0}"#).contains("missing field `kind`"));
236        assert!(
237            refused(r#"{"kind":"plugged","kind":"plugged"}"#).contains("duplicate field `kind`")
238        );
239        assert!(
240            refused(r#"{"kind":"seconds","value":1.0,"value":2.0}"#)
241                .contains("duplicate field `value`")
242        );
243        assert!(refused(r#"{"kind":"plugged","x":0}"#).contains("unknown variant `x`"));
244        assert!(refused(r#"{"kind":"later"}"#).contains("unknown variant `later`"));
245        assert!(refused(r#"["plugged"]"#).contains("invalid type: sequence"));
246        assert!(refused(r#"["seconds", 6.0]"#).contains("invalid type: sequence"));
247    }
248
249    #[test]
250    fn malformed_pieces_are_dropped_with_warnings() {
251        let list = DelayList::parse("4-7-10,");
252        assert_eq!(list.delays.len(), 3);
253        assert_eq!(list.warnings.len(), 1);
254        let list = DelayList::parse("1-3--4");
255        assert_eq!(list.delays.len(), 3);
256        assert_eq!(list.warnings.len(), 1);
257        let list = DelayList::parse("-");
258        assert!(list.delays.is_empty());
259        assert_eq!(list.warnings.len(), 2);
260        let list = DelayList::parse("S-M-L");
261        assert!(list.delays.is_empty());
262        assert_eq!(list.warnings.len(), 3);
263        let list = DelayList::parse("0");
264        assert_eq!(list.delays, [Delay::ZeroOrPlugged]);
265        assert!(!list.has_plugged());
266        assert_eq!(list.warnings.len(), 1);
267        assert_eq!(list.warnings[0].kind, WarningKind::Unusual);
268        assert_eq!(DelayList::parse("0.0-6").delays[0], Delay::ZeroOrPlugged);
269        assert_eq!(DelayList::parse("x").warnings[0].kind, WarningKind::Dropped);
270    }
271}