Skip to main content

deep_time/time_parts/
from_ccsds_bin.rs

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