Skip to main content

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