Skip to main content

deep_time/time_parts/
from_bin_ccsds.rs

1use crate::{Dt, DtErr, DtErrKind, Scale, TimeParts, an_err};
2
3impl TimeParts {
4    /// Parses a CCSDS Calendar Segmented Time Code (CCS) into [`TimeParts`].
5    ///
6    /// Implements **CCSDS 301.0-B-4 §3.4 (Level 1 only)**.
7    ///
8    /// This function accepts a single-byte P-field followed by a BCD-encoded T-field.
9    /// It supports both calendar variants:
10    /// - Month/Day format (most common)
11    /// - Day-of-Year format
12    ///
13    /// ## P-field
14    ///
15    /// - Must not have the extension bit set (only 1-byte P-fields are supported).
16    /// - Code ID must be `101`.
17    /// - Subsecond resolution: 0 to 6 BCD octets (0–12 decimal digits).
18    ///
19    /// ## T-field
20    ///
21    /// - Year is encoded as 4 BCD digits (0001–9999).
22    /// - Time of day uses BCD with leap second support (`second == 60`).
23    /// - When a leap second is present, `second` is normalized to 59 and
24    ///   `is_leap_second` is set to `true` in the returned [`TimeParts`].
25    ///
26    /// ## Epoch
27    ///
28    /// 1958-01-01 00:00:00 UTC (identical to CDS).
29    ///
30    /// ## Errors
31    ///
32    /// Returns an error if the P-field is extended, the Code ID is wrong,
33    /// BCD digits are invalid, field lengths are insufficient, or any
34    /// component (month, day, DOY, hour, minute, second) is out of range.
35    ///
36    /// The resulting [`TimeParts`] has `scale = UTC`.
37    pub fn from_ccsds_ccs(input: &[u8]) -> Result<TimeParts, DtErr> {
38        if input.is_empty() {
39            return Err(an_err!(DtErrKind::Incomplete, "empty"));
40        }
41
42        let p1 = input[0];
43        let mut idx = 1usize;
44
45        if (p1 & 0b1000_0000) != 0 {
46            return Err(an_err!(
47                DtErrKind::InvalidInput,
48                "p-field ext. not supported"
49            ));
50        }
51
52        let code_id = (p1 >> 4) & 0b0111;
53        if code_id != 0b101 {
54            return Err(an_err!(DtErrKind::InvalidItem, "code id"));
55        }
56
57        let is_doy = ((p1 >> 3) & 1) != 0;
58        let n_subsec = (p1 & 0b0000_0111) as usize;
59
60        if n_subsec > 6 {
61            return Err(an_err!(DtErrKind::InvalidItem, "subsecond count"));
62        }
63
64        let min_len = 1 + 2 + 2 + 3 + n_subsec;
65        if input.len() < min_len {
66            return Err(an_err!(DtErrKind::InvalidSyntax, "t-field too short"));
67        }
68
69        let bcd_byte = |b: u8| -> Result<u8, DtErr> {
70            let hi = b >> 4;
71            let lo = b & 0x0F;
72            if hi > 9 || lo > 9 {
73                Err(an_err!(DtErrKind::InvalidBytes, "invalid bcd digit"))
74            } else {
75                Ok(hi * 10 + lo)
76            }
77        };
78
79        // Year
80        let y1 = bcd_byte(input[idx])?;
81        let y2 = bcd_byte(input[idx + 1])?;
82        let year = (y1 as i64) * 100 + (y2 as i64);
83        idx += 2;
84
85        // Date field
86        let (month, day, day_of_year) = if !is_doy {
87            let mo = bcd_byte(input[idx])?;
88            let d = bcd_byte(input[idx + 1])?;
89            idx += 2;
90
91            if !(1..=12).contains(&mo) {
92                return Err(an_err!(DtErrKind::OutOfRange, "month"));
93            }
94            if !(1..=31).contains(&d) {
95                return Err(an_err!(DtErrKind::OutOfRange, "day"));
96            }
97            (Some(mo), Some(d), None)
98        } else {
99            let d1 = bcd_byte(input[idx])?;
100            let d2 = bcd_byte(input[idx + 1])?;
101            idx += 2;
102            let doy = (d1 as u16) * 100 + (d2 as u16);
103
104            if doy == 0 || doy > 366 || (doy == 366 && !Dt::is_leap_yr(year)) {
105                return Err(an_err!(DtErrKind::OutOfRange, "day of year"));
106            }
107            (None, None, Some(doy))
108        };
109
110        // Time
111        let hour = bcd_byte(input[idx])?;
112        let minute = bcd_byte(input[idx + 1])?;
113        let mut second = bcd_byte(input[idx + 2])?;
114        idx += 3;
115
116        if hour > 23 {
117            return Err(an_err!(DtErrKind::OutOfRange, "hour"));
118        }
119        if minute > 59 {
120            return Err(an_err!(DtErrKind::OutOfRange, "minute"));
121        }
122
123        if second == 60 {
124            second = 59;
125        } else if second > 59 {
126            return Err(an_err!(DtErrKind::OutOfRange, "second"));
127        }
128
129        // Subseconds (BCD → attoseconds)
130        let mut frac_value: u128 = 0;
131        for _ in 0..n_subsec {
132            let b = input[idx];
133            let hi = (b >> 4) as u128;
134            let lo = (b & 0x0F) as u128;
135            if hi > 9 || lo > 9 {
136                return Err(an_err!(DtErrKind::InvalidBytes, "invalid subsecond bcd"));
137            }
138            frac_value = frac_value * 100 + hi * 10 + lo;
139            idx += 1;
140        }
141
142        let attos = if n_subsec == 0 {
143            0
144        } else {
145            let decimal_places = (2 * n_subsec) as u32;
146            let denom = 10u128.pow(decimal_places);
147            ((frac_value * 1_000_000_000_000_000_000u128) / denom) as u64
148        };
149
150        let mut pd = TimeParts {
151            yr: Some(year),
152            mo: month,
153            day,
154            day_of_yr: day_of_year,
155            hr: hour,
156            min: minute,
157            sec: second,
158            attos,
159            scale: Scale::UTC,
160            ..TimeParts::default()
161        };
162
163        pd.finish(false)?;
164        Ok(pd)
165    }
166
167    /// Parses a **CCSDS C (CUC – Unsegmented Time Code)** binary time code
168    /// into a [`TimeParts`].
169    ///
170    /// Implements **CCSDS 301.0-B-4 §3.2 (Level 1)**, including full support
171    /// for the extended 2-byte P-field.
172    ///
173    /// ## Supported formats (Level 1 only)
174    ///
175    /// - 1-byte or 2-byte P-field (further extension beyond 2 bytes is rejected).
176    /// - Code ID must be `001` (1958-01-01 TAI epoch).
177    /// - Coarse time: 1–7 octets total.
178    /// - Fractional time: 0–10 octets total.
179    ///
180    /// ## P-field decoding
181    ///
182    /// - **First octet (P1)**:
183    ///   - Bit 7:     Extension flag (1 = second P-field octet follows)
184    ///   - Bits 6-4:  Code ID (must be `001`)
185    ///   - Bits 3-2:  Coarse time octets minus 1 (0–3 → 1–4 octets)
186    ///   - Bits 1-0:  Fractional time octets (0–3)
187    ///
188    /// - **Second octet (P2, when extension flag is set)**:
189    ///   - Bit 7:     Further-extension flag (must be 0; 3+-byte P-fields are rejected)
190    ///   - Bits 6-5:  Additional coarse octets (0–3)
191    ///   - Bits 4-2:  Additional fractional octets (0–7)
192    ///   - Bits 1-0:  Reserved (ignored)
193    ///
194    /// ## T-field
195    ///
196    /// - Coarse time is interpreted as seconds since **1958-01-01 00:00:00 TAI**.
197    /// - Fractional time is converted to attoseconds using exact integer arithmetic
198    ///   (`value × 10¹⁸ / 2^(8·n_frac)`).
199    ///
200    /// ## Returns
201    ///
202    /// A [`TimeParts`] with `scale = TAI` and the decoded civil date/time.
203    ///
204    /// ## Errors
205    ///
206    /// - [`DtErrKind::Incomplete`] if `input` is empty.
207    /// - [`DtErrKind::InvalidItem`] if the Code ID is not `001`.
208    /// - [`DtErrKind::InvalidInput`] if the input is too short to contain the declared
209    ///   extended P-field, or if the "further extension" flag (bit 7 of the second
210    ///   P-field octet) is set.
211    /// - [`DtErrKind::InvalidSyntax`] if the declared coarse + fractional field lengths
212    ///   make the T-field longer than the remaining input bytes.
213    pub fn from_ccsds_cuc(input: &[u8]) -> Result<TimeParts, DtErr> {
214        if input.is_empty() {
215            return Err(an_err!(DtErrKind::Incomplete, "empty"));
216        }
217
218        let p1 = input[0];
219        let mut idx = 1usize;
220
221        let extension = (p1 & 0b1000_0000) != 0;
222        let code_id = (p1 >> 4) & 0b0111;
223        if code_id != 0b001 {
224            return Err(an_err!(DtErrKind::InvalidItem, "code id"));
225        }
226
227        let base_coarse = (((p1 >> 2) & 0b0011) as usize) + 1;
228        let base_frac = (p1 & 0b0011) as usize;
229
230        let (n_coarse, n_frac) = if extension {
231            if input.len() < 2 {
232                return Err(an_err!(DtErrKind::InvalidInput, "p-field too short"));
233            }
234            let p2 = input[1];
235            idx += 1;
236
237            if (p2 & 0b1000_0000) != 0 {
238                return Err(an_err!(
239                    DtErrKind::InvalidInput,
240                    "further p-field ext. not supported"
241                ));
242            }
243
244            let add_coarse = ((p2 >> 5) & 0b0000_0011) as usize;
245            let add_frac = ((p2 >> 2) & 0b0000_0111) as usize;
246
247            (base_coarse + add_coarse, base_frac + add_frac)
248        } else {
249            (base_coarse, base_frac)
250        };
251
252        if n_coarse == 0 || input.len() < idx + n_coarse + n_frac {
253            return Err(an_err!(DtErrKind::InvalidSyntax, "t-field too short"));
254        }
255
256        // Read coarse time (big-endian)
257        let mut coarse_sec: u64 = 0;
258        for _ in 0..n_coarse {
259            coarse_sec = (coarse_sec << 8) | u64::from(input[idx]);
260            idx += 1;
261        }
262
263        // Read fractional time (big-endian)
264        let mut frac_raw: u128 = 0;
265        for _ in 0..n_frac {
266            frac_raw = (frac_raw << 8) | u128::from(input[idx]);
267            idx += 1;
268        }
269
270        let frac_attos = if n_frac == 0 {
271            0
272        } else {
273            let denom = 1u128 << (8 * n_frac as u32);
274            let numerator = frac_raw * 1_000_000_000_000_000_000u128;
275            // Add proper rounding (symmetric to the encoder)
276            ((numerator + (denom / 2)) / denom) as u64
277        };
278
279        // Convert to civil time using custom Gregorian conversion
280        let days_since_epoch = (coarse_sec / 86400) as i64;
281        let sec_of_day = (coarse_sec % 86400) as i64;
282
283        let (year, month, day) = Dt::days_since_1958_to_gregorian(days_since_epoch);
284
285        let hour = (sec_of_day / 3600) as u8;
286        let minute = ((sec_of_day % 3600) / 60) as u8;
287        let second = (sec_of_day % 60) as u8;
288
289        let mut pd = TimeParts {
290            yr: Some(year),
291            mo: Some(month),
292            day: Some(day),
293            hr: hour,
294            min: minute,
295            sec: second,
296            attos: frac_attos,
297            scale: Scale::TAI,
298            ..TimeParts::default()
299        };
300        pd.finish(false)?;
301        Ok(pd)
302    }
303
304    /// Parses a **CCSDS D (CDS – Day Segmented Time Code)** binary time code
305    /// into a [`TimeParts`].
306    ///
307    /// Implements **CCSDS 301.0-B-4 §3.3 (Level 1)**.
308    ///
309    /// ## Supported formats (Level 1 only)
310    ///
311    /// - 1-byte or 2-byte P-field.
312    /// - Code ID must be `100` and the Epoch bit must be `0` (1958-01-01 UTC epoch).
313    /// - Day count: 2 or 3 bytes.
314    /// - Milliseconds since midnight: always 4 bytes.
315    /// - Sub-millisecond field (bits 1-0 of P-field):
316    ///   - `00`: no fractional field
317    ///   - `01`: 2 bytes (microseconds within the millisecond, 0–65535)
318    ///   - `10`: 4 bytes (fractional part of the millisecond as 2⁻³²)
319    ///   - `11`: rejected (unsupported)
320    ///
321    /// ## P-field bit layout (first octet)
322    ///
323    /// - Bit 7:     Extension flag (1 = second P-field octet follows)
324    /// - Bits 6-4:  Code ID (must be `100`)
325    /// - Bit 3:     Epoch (must be `0` for Level 1 / 1958 epoch)
326    /// - Bit 2:     Day count size (`0` = 2 bytes, `1` = 3 bytes)
327    /// - Bits 1-0:  Sub-millisecond code (see above)
328    ///
329    /// ## T-field
330    ///
331    /// - Day count is days since **1958-01-01 00:00:00 UTC**.
332    /// - Milliseconds since midnight are always present (4 bytes).
333    /// - Sub-millisecond data (if present) is converted to attoseconds with
334    ///   exact integer scaling.
335    ///
336    /// ## Leap-second handling
337    ///
338    /// Correctly supports leap seconds. When the millisecond-of-day value
339    /// represents 23:59:60 (i.e. `millis_of_day >= 86_400_000`), `sec` is set
340    /// to `60` and `is_leap_second` is effectively indicated via the `sec` field
341    /// in the returned [`TimeParts`].
342    ///
343    /// ## Returns
344    ///
345    /// A [`TimeParts`] with `scale = UTC` and the decoded civil date/time.
346    ///
347    /// ## Errors
348    ///
349    /// - [`DtErrKind::Incomplete`] if `input` is empty.
350    /// - [`DtErrKind::InvalidInput`] if the P-field indicates an extended second
351    ///   octet but the input is too short to contain it.
352    /// - [`DtErrKind::InvalidItem`] if the Code ID is not `100`, the Epoch bit is
353    ///   set (non-Level-1 epoch), or the sub-millisecond code is `0b11`.
354    /// - [`DtErrKind::InvalidSyntax`] if the declared field lengths make the
355    ///   T-field longer than the remaining input bytes.
356    pub fn from_ccsds_cds(input: &[u8]) -> Result<TimeParts, DtErr> {
357        if input.is_empty() {
358            return Err(an_err!(DtErrKind::Incomplete, "empty"));
359        }
360
361        let p1 = input[0];
362        let mut idx = 1usize;
363
364        let extension = (p1 & 0b1000_0000) != 0;
365        if extension {
366            if input.len() < 2 {
367                return Err(an_err!(DtErrKind::InvalidInput, "p-field too short"));
368            }
369            idx += 1;
370        }
371
372        let code_id = (p1 >> 4) & 0b0111;
373        if code_id != 0b100 {
374            return Err(an_err!(DtErrKind::InvalidItem, "code id"));
375        }
376
377        if (p1 & 0b0000_1000) != 0 {
378            return Err(an_err!(
379                DtErrKind::InvalidItem,
380                "non-level-1 epoch not supported"
381            ));
382        }
383
384        let n_day = if (p1 & 0b0000_0100) == 0 { 2 } else { 3 };
385        let sub_ms_code = p1 & 0b0000_0011;
386
387        let n_subsec = match sub_ms_code {
388            0b00 => 0,
389            0b01 => 2,
390            0b10 => 4,
391            _ => return Err(an_err!(DtErrKind::InvalidItem, "sub-millisecond code")),
392        };
393
394        if input.len() < idx + n_day + 4 + n_subsec {
395            return Err(an_err!(DtErrKind::InvalidSyntax, "t-field too short"));
396        }
397
398        // Read fields
399        let mut day_count: u64 = 0;
400        for _ in 0..n_day {
401            day_count = (day_count << 8) | u64::from(input[idx]);
402            idx += 1;
403        }
404
405        let mut millis_of_day: u64 = 0;
406        for _ in 0..4 {
407            millis_of_day = (millis_of_day << 8) | u64::from(input[idx]);
408            idx += 1;
409        }
410
411        let mut frac_raw: u64 = 0;
412        for _ in 0..n_subsec {
413            frac_raw = (frac_raw << 8) | u64::from(input[idx]);
414            idx += 1;
415        }
416
417        // === Leap second handling (robust) ===
418        let total_sec_in_day = millis_of_day / 1000;
419        let is_leap_second = total_sec_in_day == 86400;
420
421        let effective_sec = if is_leap_second {
422            86399
423        } else {
424            total_sec_in_day
425        };
426
427        let sec_of_day = effective_sec;
428        let remaining_ms = (millis_of_day % 1000) as u128;
429
430        // Sub-millisecond to attoseconds
431        let sub_ms_attos = if n_subsec == 0 {
432            0
433        } else if sub_ms_code == 0b01 {
434            (frac_raw as u128 * 1_000_000_000_000_000) / 65_536
435        } else {
436            (frac_raw as u128 * 1_000_000_000_000_000) / (1u128 << 32)
437        };
438
439        let frac_attos = remaining_ms * 1_000_000_000_000_000 + sub_ms_attos;
440
441        // Convert day count to Gregorian
442        let days_since_epoch = day_count as i64;
443        let (year, month, day) = Dt::days_since_1958_to_gregorian(days_since_epoch);
444
445        let hour = (sec_of_day / 3600) as u8;
446        let minute = ((sec_of_day % 3600) / 60) as u8;
447        let mut second = (sec_of_day % 60) as u8;
448
449        if is_leap_second {
450            second = 60;
451        }
452
453        let mut pd = TimeParts {
454            yr: Some(year),
455            mo: Some(month),
456            day: Some(day),
457            hr: hour,
458            min: minute,
459            sec: second,
460            attos: frac_attos as u64,
461            scale: Scale::UTC,
462            ..TimeParts::default()
463        };
464        pd.finish(false)?;
465        Ok(pd)
466    }
467
468    /// Auto-detects and parses a CCSDS binary time code (CUC, CDS, or CCS)
469    /// based on the Code ID in the first P-field byte.
470    ///
471    /// Examines the Code ID and dispatches to the appropriate parser:
472    /// - `001` → [`from_ccsds_cuc`](Self::from_ccsds_cuc) (CUC – Unsegmented Time Code)
473    /// - `100` → [`from_ccsds_cds`](Self::from_ccsds_cds) (CDS – Day Segmented Time Code)
474    /// - `101` → [`from_ccsds_ccs`](Self::from_ccsds_ccs) (CCS – Calendar Segmented Time Code)
475    ///
476    /// This is a convenience wrapper. For stricter control or when the format
477    /// is known in advance, prefer calling the specific `from_ccsds_*` function directly.
478    ///
479    /// ## Errors
480    ///
481    /// - [`DtErrKind::Incomplete`] if `input` is empty.
482    /// - [`DtErrKind::InvalidItem`] if the Code ID is not one of the three
483    ///   recognized Level 1 values (`001`, `100`, or `101`).
484    ///
485    /// The resulting [`TimeParts`] has `scale` set according to the detected format
486    /// (TAI for CUC, UTC for CDS, and format-dependent for CCS).
487    pub fn from_ccsds_bin(input: &[u8]) -> Result<TimeParts, DtErr> {
488        if input.is_empty() {
489            return Err(an_err!(DtErrKind::Incomplete, "empty"));
490        }
491        let code_id = (input[0] >> 4) & 0b0111;
492        match code_id {
493            0b001 => Self::from_ccsds_cuc(input),
494            0b100 => Self::from_ccsds_cds(input),
495            0b101 => Self::from_ccsds_ccs(input),
496            _ => Err(an_err!(DtErrKind::InvalidItem, "unknown code id")),
497        }
498    }
499}