Skip to main content

deep_time/time_parts/
from_str_ccsds.rs

1use crate::{DtErr, DtErrKind, 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    ///     - e.g. 2000-01-01T12:00:00
7    /// - All time components after the date portion are optional.
8    pub fn from_str_ccsds(input: &str) -> Result<Self, DtErr> {
9        let cleaned = input.trim_end_matches(|c: char| c.eq_ignore_ascii_case(&'Z'));
10        let bytes = cleaned.as_bytes();
11        let len_ = bytes.len();
12
13        let mut fmt_buf: [u8; 64] = [0; 64];
14        let mut fmt_len: usize = 0;
15        let mut pos: usize = 0;
16
17        // Year (exactly 4 digits)
18        if pos + 4 > len_ || !bytes[pos..pos + 4].iter().all(|&b| b.is_ascii_digit()) {
19            return Err(an_err!(DtErrKind::ExpectedValue, "4-digit year"));
20        }
21        fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%Y");
22        fmt_len += 2;
23        pos += 4;
24
25        // Required separator after year
26        if pos < len_ && !bytes[pos].is_ascii_digit() {
27            fmt_buf[fmt_len] = bytes[pos];
28            fmt_len += 1;
29            pos += 1;
30        }
31
32        // DOY vs calendar date
33        let is_doy =
34            pos + 3 == len_ || (pos + 3 < len_ && matches!(bytes[pos + 3], b' ' | b'T' | b't'));
35
36        if is_doy {
37            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%j");
38            fmt_len += 2;
39            pos += 3;
40        } else {
41            // %m
42            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
43                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit month"));
44            }
45            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%m");
46            fmt_len += 2;
47            pos += 2;
48
49            if pos < len_ && !bytes[pos].is_ascii_digit() {
50                fmt_buf[fmt_len] = bytes[pos];
51                fmt_len += 1;
52                pos += 1;
53            }
54
55            // %d
56            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
57                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit day"));
58            }
59            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%d");
60            fmt_len += 2;
61            pos += 2;
62        }
63
64        // Date-time separator
65        if pos < len_ {
66            let c = bytes[pos];
67            if matches!(c, b'T' | b't' | b' ') {
68                fmt_buf[fmt_len] = c;
69                fmt_len += 1;
70                pos += 1;
71            } else {
72                return Err(an_err!(
73                    DtErrKind::InvalidSyntax,
74                    "expected T/t/space separator"
75                ));
76            }
77        }
78
79        // Optional time: %H [: %M [: %S [.%.f]]]
80
81        if pos + 2 <= len_ {
82            if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
83                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit hour"));
84            }
85            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%H");
86            fmt_len += 2;
87            pos += 2;
88        }
89
90        if pos < len_ && !bytes[pos].is_ascii_digit() {
91            fmt_buf[fmt_len] = bytes[pos];
92            fmt_len += 1;
93            pos += 1;
94        }
95
96        if pos + 2 <= len_ {
97            if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
98                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit minute"));
99            }
100            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%M");
101            fmt_len += 2;
102            pos += 2;
103        }
104
105        if pos < len_ && !bytes[pos].is_ascii_digit() {
106            fmt_buf[fmt_len] = bytes[pos];
107            fmt_len += 1;
108            pos += 1;
109        }
110
111        if pos + 2 <= len_ {
112            if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
113                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit second"));
114            }
115            fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%S");
116            fmt_len += 2;
117            pos += 2;
118        }
119
120        // fractional seconds
121        if pos < len_ {
122            if bytes[pos] == b'.' {
123                fmt_buf[fmt_len..fmt_len + 3].copy_from_slice(b"%.f");
124                fmt_len += 3;
125                pos += 1;
126            } else {
127                fmt_buf[fmt_len..fmt_len + 2].copy_from_slice(b"%f");
128                fmt_len += 2;
129            }
130            while pos < len_ && bytes[pos].is_ascii_digit() {
131                pos += 1;
132            }
133        }
134
135        let format = match core::str::from_utf8(&fmt_buf[0..fmt_len]) {
136            Ok(f) => f,
137            Err(_) => {
138                return Err(an_err!(DtErrKind::InvalidBytes, "from utf8"));
139            }
140        };
141
142        TimeParts::from_str(format, cleaned, false, false, false)
143    }
144}