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