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}