Skip to main content

deep_time/time_parts/
from_str_ccsds.rs

1use crate::{DtErr, DtErrKind, Scale, TimeParts, an_err};
2
3impl TimeParts {
4    /// Generalized CCSDS ASCII Time Code parser (A or B variant).
5    /// - Handles both calendar (`%Y-%m-%d`) and day-of-year (`%Y-%j`) formats.
6    /// - Example formats:
7    ///     - 2000-01-01T12:00:00
8    ///     - 2000-001T12:00:00
9    /// - All time components after the date portion are optional.
10    /// - Optional time scale on the end also supported.
11    pub fn from_str_ccsds(input: &str) -> Result<Self, DtErr> {
12        let bytes = input.as_bytes();
13        let len_ = bytes.len();
14
15        let mut start = 0usize;
16        while start < len_ {
17            let b = bytes[start];
18            if b.is_ascii_digit()
19                || (matches!(b, b'+' | b'-')
20                    && start + 1 < len_
21                    && bytes[start + 1].is_ascii_digit())
22            {
23                break;
24            }
25            start += 1;
26        }
27
28        if start == len_ {
29            return Err(an_err!(
30                DtErrKind::ExpectedValue,
31                "year start (digit or +/- and digit)"
32            ));
33        }
34
35        let input = &input[start..];
36        let bytes = input.as_bytes();
37        let len_ = bytes.len();
38        let mut fmt_buf: [u8; 128] = [0; 128];
39        let mut fmt_len: usize = 0;
40        let mut pos: usize = 0;
41
42        // Year: optional sign (+/-), then ≥1 digits.
43        if pos < len_ && matches!(bytes[pos], b'+' | b'-') {
44            pos += 1;
45        }
46        let year_start = pos;
47        while pos < len_ && bytes[pos].is_ascii_digit() {
48            pos += 1;
49        }
50        let year_len = pos - year_start;
51        if year_len == 0 {
52            return Err(an_err!(
53                DtErrKind::ExpectedValue,
54                "year (digits after optional sign)"
55            ));
56        }
57        fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%*");
58        fmt_len += 2;
59
60        // Required separator after year
61        if pos < len_ && !bytes[pos].is_ascii_digit() {
62            fmt_buf[fmt_len] = bytes[pos];
63            fmt_len += 1;
64            pos += 1;
65        }
66
67        // DOY vs calendar date
68        let is_doy = pos + 3 == len_ || pos + 3 < len_ && !bytes[pos + 3].is_ascii_digit();
69
70        if is_doy {
71            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%j");
72            fmt_len += 2;
73            pos += 3;
74        } else {
75            // %m
76            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
77                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit month"));
78            }
79            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%m");
80            fmt_len += 2;
81            pos += 2;
82
83            if pos < len_ && !bytes[pos].is_ascii_digit() {
84                fmt_buf[fmt_len] = bytes[pos];
85                fmt_len += 1;
86                pos += 1;
87            }
88
89            // %d
90            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
91                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit day"));
92            }
93            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%d");
94            fmt_len += 2;
95            pos += 2;
96        }
97
98        // Date-time separator
99        if pos < len_ {
100            let c = bytes[pos];
101            if !c.is_ascii_digit() {
102                // perhaps time scale and end, check if char after is digit
103                if pos + 1 < len_ && bytes[pos + 1].is_ascii_digit() {
104                    fmt_buf[fmt_len] = c;
105                    fmt_len += 1;
106                    pos += 1;
107                }
108            } else {
109                return Err(an_err!(
110                    DtErrKind::InvalidSyntax,
111                    "expected time separator e.g. T"
112                ));
113            }
114        }
115
116        // Optional time: %H [: %M [: %S [.%.f]]]
117        if pos < len_ && bytes[pos].is_ascii_digit() {
118            if pos + 2 <= len_ {
119                if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
120                    return Err(an_err!(DtErrKind::ExpectedValue, "2-digit hour"));
121                }
122                fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%H");
123                fmt_len += 2;
124                pos += 2;
125            }
126
127            if pos < len_ && !bytes[pos].is_ascii_digit() {
128                fmt_buf[fmt_len] = bytes[pos];
129                fmt_len += 1;
130                pos += 1;
131            }
132
133            if pos + 2 <= len_ {
134                if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
135                    return Err(an_err!(DtErrKind::ExpectedValue, "2-digit minute"));
136                }
137                fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%M");
138                fmt_len += 2;
139                pos += 2;
140            }
141
142            if pos < len_ && !bytes[pos].is_ascii_digit() {
143                fmt_buf[fmt_len] = bytes[pos];
144                fmt_len += 1;
145                pos += 1;
146            }
147
148            if pos + 2 <= len_ {
149                if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
150                    return Err(an_err!(DtErrKind::ExpectedValue, "2-digit second"));
151                }
152                fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%S");
153                fmt_len += 2;
154                pos += 2;
155            }
156
157            // fractional seconds
158            if pos < len_ {
159                if bytes[pos] == b'.' {
160                    fmt_buf[fmt_len..fmt_len + 3].copy_from_slice(b"%.f");
161                    fmt_len += 3;
162                    pos += 1;
163                } else if bytes[pos].is_ascii_digit() {
164                    fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%f");
165                    fmt_len += 2;
166                }
167                while pos < len_ && bytes[pos].is_ascii_digit() {
168                    pos += 1;
169                }
170            }
171
172            // skip optional Z
173            if pos + 1 < len_ && matches!(bytes[pos], b'Z' | b'z') {
174                fmt_buf[fmt_len] = bytes[pos];
175                fmt_len += 1;
176                pos += 1;
177            }
178        }
179
180        if pos < len_ {
181            // skip optional whitespace separator
182            if pos < len_ && !bytes[pos].is_ascii_alphabetic() {
183                fmt_buf[fmt_len] = bytes[pos];
184                fmt_len += 1;
185                pos += 1;
186            }
187            if pos < len_ {
188                let end = {
189                    let mut i = pos;
190                    while i < len_ && bytes[i].is_ascii_alphabetic() {
191                        i += 1;
192                        if i - pos > 8 {
193                            break;
194                        }
195                    }
196                    i
197                };
198                if Scale::from_abbrev(&input[pos..end]).is_some() {
199                    fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%L");
200                    fmt_len += 2;
201                    pos += end - pos;
202                }
203            }
204        }
205
206        let format = match core::str::from_utf8(&fmt_buf[0..fmt_len]) {
207            Ok(f) => f,
208            Err(_) => {
209                return Err(an_err!(DtErrKind::InvalidBytes, "from utf8"));
210            }
211        };
212
213        TimeParts::from_str(format, &input[..pos], true, true, false)
214    }
215}