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    /// - All time components after the date portion are optional.
7    /// - Optional time scale on the end is also supported.
8    pub fn from_str_ccsds(input: &str) -> Result<Self, DtErr> {
9        let bytes = input.as_bytes();
10        let len_ = bytes.len();
11
12        let mut start = 0usize;
13        while start < len_ {
14            let b = bytes[start];
15            if b.is_ascii_digit()
16                || (matches!(b, b'+' | b'-')
17                    && start + 1 < len_
18                    && bytes[start + 1].is_ascii_digit())
19            {
20                break;
21            }
22            start += 1;
23        }
24
25        if start == len_ {
26            return Err(an_err!(
27                DtErrKind::ExpectedValue,
28                "year start (digit or +/- and digit)"
29            ));
30        }
31
32        let input = &input[start..];
33        let bytes = input.as_bytes();
34        let len_ = bytes.len();
35        let mut pos: usize = 0;
36        let mut tp = TimeParts::new_utc();
37
38        // Year (manual accumulation, optional sign)
39        let mut year: i64 = 0;
40        let negative_year = pos < len_ && bytes[pos] == b'-';
41
42        if pos < len_ && matches!(bytes[pos], b'+' | b'-') {
43            pos += 1;
44        }
45
46        let mut has_year_digit = false;
47        while pos < len_ && bytes[pos].is_ascii_digit() {
48            has_year_digit = true;
49            year = year * 10 + (bytes[pos] - b'0') as i64;
50            pos += 1;
51        }
52        if !has_year_digit {
53            return Err(an_err!(
54                DtErrKind::ExpectedValue,
55                "year (digits after optional sign)"
56            ));
57        }
58        if negative_year {
59            year = -year;
60        }
61        tp.yr = Some(year);
62
63        // Optional separator after year (consume only if present)
64        if pos < len_ && !bytes[pos].is_ascii_digit() {
65            pos += 1;
66        }
67
68        // DOY vs calendar detection
69        let is_doy = pos + 3 == len_ || (pos + 3 < len_ && !bytes[pos + 3].is_ascii_digit());
70
71        if is_doy {
72            // 3-digit day of year
73            if pos + 3 > len_ || !bytes[pos..pos + 3].iter().all(|&b| b.is_ascii_digit()) {
74                return Err(an_err!(DtErrKind::ExpectedValue, "3-digit day of year"));
75            }
76            let mut doy: u16 = 0;
77            for _ in 0..3 {
78                doy = doy * 10 + (bytes[pos] - b'0') as u16;
79                pos += 1;
80            }
81            tp.day_of_yr = Some(doy);
82        } else {
83            // 2-digit month
84            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
85                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit month"));
86            }
87            let mut mo: u8 = 0;
88            for _ in 0..2 {
89                mo = mo * 10 + (bytes[pos] - b'0');
90                pos += 1;
91            }
92            tp.mo = Some(mo);
93
94            // Optional separator after month
95            if pos < len_ && !bytes[pos].is_ascii_digit() {
96                pos += 1;
97            }
98
99            // 2-digit day
100            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
101                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit day"));
102            }
103            let mut day: u8 = 0;
104            for _ in 0..2 {
105                day = day * 10 + (bytes[pos] - b'0');
106                pos += 1;
107            }
108            tp.day = Some(day);
109        }
110
111        // Optional date-time separator (only consume if followed by a digit)
112        if pos < len_ {
113            let c = bytes[pos];
114            if !c.is_ascii_digit() {
115                if pos + 1 < len_ && bytes[pos + 1].is_ascii_digit() {
116                    pos += 1;
117                }
118            } else {
119                return Err(an_err!(
120                    DtErrKind::InvalidSyntax,
121                    "expected time separator e.g. T"
122                ));
123            }
124        }
125
126        // Optional time components
127        if pos < len_ && bytes[pos].is_ascii_digit() {
128            // Hour (2 digits)
129            if pos + 2 > len_ || !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
130                return Err(an_err!(DtErrKind::ExpectedValue, "2-digit hour"));
131            }
132            let mut hr: u8 = 0;
133            for _ in 0..2 {
134                hr = hr * 10 + (bytes[pos] - b'0');
135                pos += 1;
136            }
137            tp.hr = hr;
138
139            if pos < len_ && !bytes[pos].is_ascii_digit() {
140                pos += 1;
141            }
142
143            // Minute (2 digits, if present)
144            if pos + 2 <= len_ {
145                if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
146                    return Err(an_err!(DtErrKind::ExpectedValue, "2-digit minute"));
147                }
148                let mut min: u8 = 0;
149                for _ in 0..2 {
150                    min = min * 10 + (bytes[pos] - b'0');
151                    pos += 1;
152                }
153                tp.min = min;
154            }
155
156            if pos < len_ && !bytes[pos].is_ascii_digit() {
157                pos += 1;
158            }
159
160            // Second (2 digits, if present)
161            if pos + 2 <= len_ {
162                if !bytes[pos..pos + 2].iter().all(|&b| b.is_ascii_digit()) {
163                    return Err(an_err!(DtErrKind::ExpectedValue, "2-digit second"));
164                }
165                let mut sec: u8 = 0;
166                for _ in 0..2 {
167                    sec = sec * 10 + (bytes[pos] - b'0');
168                    pos += 1;
169                }
170                tp.sec = sec;
171            }
172
173            // Fractional seconds (with or without leading dot)
174            if pos < len_ {
175                let has_dot = bytes[pos] == b'.';
176                if has_dot {
177                    pos += 1;
178                }
179
180                if pos < len_ && bytes[pos].is_ascii_digit() {
181                    let mut attos: u64 = 0;
182                    let mut digits_seen: usize = 0;
183
184                    while pos < len_ && bytes[pos].is_ascii_digit() {
185                        if digits_seen < 18 {
186                            attos = attos * 10 + (bytes[pos] - b'0') as u64;
187                            digits_seen += 1;
188                        }
189                        // Ignore any digits beyond the first 18
190                        pos += 1;
191                    }
192
193                    if digits_seen > 0 {
194                        tp.attos = attos * 10u64.pow(18u32.saturating_sub(digits_seen as u32));
195                    }
196                }
197            }
198
199            // Optional trailing Z/z
200            if pos < len_ && matches!(bytes[pos], b'Z' | b'z') {
201                pos += 1;
202            }
203        }
204
205        // Optional trailing scale (e.g. TAI, UTC)
206        if pos < len_ {
207            if pos < len_ && !bytes[pos].is_ascii_alphabetic() {
208                pos += 1;
209            }
210            if pos < len_ {
211                let end = {
212                    let mut i = pos;
213                    while i < len_ && bytes[i].is_ascii_alphabetic() {
214                        i += 1;
215                        if i - pos > 8 {
216                            break;
217                        }
218                    }
219                    i
220                };
221                if let Some(sc) = Scale::from_abbrev(&input[pos..end]) {
222                    tp.scale = sc;
223                    // pos += end - pos;
224                }
225            }
226        }
227
228        Ok(tp)
229    }
230}