1use chrono::{DateTime, NaiveDateTime, Utc};
35
36use crate::data::runtime_data::time_data_tai_seconds_is_in_leap_window;
37use crate::earth::context::TimeContext;
38use crate::foundation::error::ConversionError;
39use crate::model::scale::UTC;
40use crate::model::time::Time;
41use alloc::string::{String, ToString};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum FormatPrecision {
46 RoundHalfToEven,
48 Truncate,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct FormatOptions {
55 pub subsecond_digits: u8,
58 pub precision: FormatPrecision,
60 pub include_zulu: bool,
64}
65
66impl FormatOptions {
67 pub const SECONDS: Self = Self {
69 subsecond_digits: 0,
70 precision: FormatPrecision::Truncate,
71 include_zulu: true,
72 };
73
74 pub const fn milliseconds() -> Self {
76 Self {
77 subsecond_digits: 3,
78 precision: FormatPrecision::RoundHalfToEven,
79 include_zulu: true,
80 }
81 }
82
83 pub const fn microseconds() -> Self {
85 Self {
86 subsecond_digits: 6,
87 precision: FormatPrecision::RoundHalfToEven,
88 include_zulu: true,
89 }
90 }
91
92 pub const fn nanoseconds() -> Self {
94 Self {
95 subsecond_digits: 9,
96 precision: FormatPrecision::RoundHalfToEven,
97 include_zulu: true,
98 }
99 }
100}
101
102impl Default for FormatOptions {
103 fn default() -> Self {
104 Self::nanoseconds()
105 }
106}
107
108#[inline]
113pub fn parse_rfc3339_utc(s: &str) -> Result<Time<UTC>, ConversionError> {
114 parse_rfc3339_utc_with(s, &TimeContext::new())
115}
116
117pub fn parse_rfc3339_utc_with(s: &str, ctx: &TimeContext) -> Result<Time<UTC>, ConversionError> {
119 if let Some(after_dot) = s.find('.') {
121 let frac_start = after_dot + 1;
123 if let Some(zone_pos) = s[frac_start..].find(['Z', '+', '-']) {
124 let frac_len = zone_pos;
125 if frac_len == 0 {
126 return Err(ConversionError::OutOfRange);
127 }
128 if frac_len > 9 {
129 return Err(ConversionError::OutOfRange);
130 }
131 }
132 }
133
134 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
136 let utc = dt.with_timezone(&Utc);
137 return Time::<UTC>::try_from_chrono_with(utc, ctx);
138 }
139
140 parse_rfc3339_manual(s, ctx)
144}
145
146fn parse_rfc3339_manual(s: &str, ctx: &TimeContext) -> Result<Time<UTC>, ConversionError> {
147 if s.len() < 20 {
149 return Err(ConversionError::OutOfRange);
150 }
151 let bytes = s.as_bytes();
152 if bytes[4] != b'-'
153 || bytes[7] != b'-'
154 || (bytes[10] != b'T' && bytes[10] != b' ')
155 || bytes[13] != b':'
156 || bytes[16] != b':'
157 {
158 return Err(ConversionError::OutOfRange);
159 }
160 let year: i32 = s[..4].parse().map_err(|_| ConversionError::OutOfRange)?;
161 let month: u32 = s[5..7].parse().map_err(|_| ConversionError::OutOfRange)?;
162 let day: u32 = s[8..10].parse().map_err(|_| ConversionError::OutOfRange)?;
163 let hour: u32 = s[11..13].parse().map_err(|_| ConversionError::OutOfRange)?;
164 let minute: u32 = s[14..16].parse().map_err(|_| ConversionError::OutOfRange)?;
165 let second_str = &s[17..19];
166 let second: u32 = second_str
167 .parse()
168 .map_err(|_| ConversionError::OutOfRange)?;
169
170 let tail = &s[19..];
172 let (frac_str, zone_str) = split_fraction_and_zone(tail)?;
173 let frac_nanos = parse_fraction_nanos(frac_str)?;
174
175 if zone_str != "Z" {
176 return Err(ConversionError::OutOfRange);
179 }
180
181 if second == 60 {
184 let base = NaiveDateTime::parse_from_str(
186 &format!("{year:04}-{month:02}-{day:02}T{hour:02}:59:59.999999999"),
187 "%Y-%m-%dT%H:%M:%S%.9f",
188 )
189 .map_err(|_| ConversionError::OutOfRange)?
190 .and_utc();
191 let utc_almost = Time::<UTC>::try_from_chrono_with(base, ctx)?;
192 let shifted =
194 utc_almost.add_exact(crate::ExactDuration::from_nanos(1 + frac_nanos as i128));
195 if !time_data_tai_seconds_is_in_leap_window(
197 ctx.time_data(),
198 shifted.to_j2000s().total_seconds(),
199 ) {
200 return Err(ConversionError::InvalidLeapSecond);
201 }
202 return Ok(shifted);
203 }
204
205 if second >= 60 || minute >= 60 || hour >= 24 {
206 return Err(ConversionError::OutOfRange);
207 }
208
209 let naive_str = if frac_nanos == 0 {
210 format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}")
211 } else {
212 format!(
213 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{:09}",
214 frac_nanos
215 )
216 };
217 let parsed = if frac_nanos == 0 {
218 NaiveDateTime::parse_from_str(&naive_str, "%Y-%m-%dT%H:%M:%S")
219 } else {
220 NaiveDateTime::parse_from_str(&naive_str, "%Y-%m-%dT%H:%M:%S%.9f")
221 }
222 .map_err(|_| ConversionError::OutOfRange)?
223 .and_utc();
224 Time::<UTC>::try_from_chrono_with(parsed, ctx)
225}
226
227fn split_fraction_and_zone(tail: &str) -> Result<(&str, &str), ConversionError> {
228 if let Some(stripped) = tail.strip_prefix('.') {
229 let zone_pos = stripped
231 .find(['Z', '+', '-'])
232 .ok_or(ConversionError::OutOfRange)?;
233 let frac = &stripped[..zone_pos];
234 if frac.is_empty() {
236 return Err(ConversionError::OutOfRange);
237 }
238 let zone = &stripped[zone_pos..];
239 Ok((frac, zone))
240 } else {
241 Ok(("", tail))
242 }
243}
244
245fn parse_fraction_nanos(s: &str) -> Result<u32, ConversionError> {
246 if s.is_empty() {
247 return Ok(0);
248 }
249 if s.len() > 9 {
251 return Err(ConversionError::OutOfRange);
252 }
253 let mut padded = [b'0'; 9];
254 padded[..s.len()].copy_from_slice(s.as_bytes());
255 core::str::from_utf8(&padded)
256 .ok()
257 .and_then(|p| p.parse::<u32>().ok())
258 .ok_or(ConversionError::OutOfRange)
259}
260
261impl Time<UTC> {
262 #[inline]
266 pub fn parse_rfc3339(s: &str) -> Result<Self, ConversionError> {
267 parse_rfc3339_utc(s)
268 }
269
270 #[inline]
273 pub fn parse_rfc3339_with(s: &str, ctx: &TimeContext) -> Result<Self, ConversionError> {
274 parse_rfc3339_utc_with(s, ctx)
275 }
276
277 pub fn format_rfc3339(&self, opts: FormatOptions) -> String {
282 self.format_rfc3339_with(opts, &TimeContext::new())
283 }
284
285 pub fn format_rfc3339_with(&self, opts: FormatOptions, ctx: &TimeContext) -> String {
292 match self.try_format_rfc3339_with(opts, ctx) {
293 Ok(s) => s,
294 Err(_) => "<invalid>".to_string(),
295 }
296 }
297
298 pub fn try_format_rfc3339_with(
303 &self,
304 opts: FormatOptions,
305 ctx: &TimeContext,
306 ) -> Result<String, ConversionError> {
307 let is_leap = self.is_leap_second_with(ctx);
311 let dt = self.try_to_chrono_with(ctx)?;
312 format_utc_datetime_rfc3339(dt, is_leap, opts)
313 }
314}
315
316fn round_subsecond(nanos: u32, digits: usize, precision: FormatPrecision) -> (u32, bool) {
319 debug_assert!(digits <= 9);
320 if digits == 9 {
321 return (nanos, false);
322 }
323 let scale = 10_u32.pow(9 - digits as u32);
324 let truncated = nanos / scale;
325 let rem = nanos % scale;
326 let mut result = truncated;
327 if matches!(precision, FormatPrecision::RoundHalfToEven) {
328 let half = scale / 2;
329 if rem > half || (rem == half && truncated % 2 == 1) {
330 result = result.saturating_add(1);
331 }
332 }
333 let threshold = 10_u32.pow(digits as u32);
334 let carry = result >= threshold;
335 if carry {
336 result -= threshold;
337 }
338 (result, carry)
339}
340
341fn format_utc_datetime_rfc3339(
351 dt: DateTime<Utc>,
352 is_leap: bool,
353 opts: FormatOptions,
354) -> Result<String, crate::foundation::error::ConversionError> {
355 use crate::foundation::error::ConversionError;
356 let digits = opts.subsecond_digits.min(9) as usize;
357 let raw_nanos = dt.timestamp_subsec_nanos();
358
359 if is_leap {
360 if raw_nanos < 1_000_000_000 {
365 return Err(ConversionError::InvalidLeapSecond);
366 }
367 let leap_nanos = raw_nanos - 1_000_000_000;
368 let (frac, carry) = round_subsecond(leap_nanos, digits, opts.precision);
369 if carry {
370 let next = dt + chrono::TimeDelta::try_seconds(1).unwrap_or_default();
373 return Ok(format_normal_dt(next, 0, digits, opts));
374 }
375 let date = dt.format("%Y-%m-%d");
376 if digits == 0 {
377 let zulu = if opts.include_zulu { "Z" } else { "" };
378 Ok(format!("{date}T23:59:60{zulu}"))
379 } else {
380 let zulu = if opts.include_zulu { "Z" } else { "" };
381 Ok(format!(
382 "{date}T23:59:60.{:0width$}{zulu}",
383 frac,
384 width = digits
385 ))
386 }
387 } else {
388 let (frac, carry) = round_subsecond(raw_nanos, digits, opts.precision);
389 let effective_dt = if carry {
390 dt + chrono::TimeDelta::try_seconds(1).unwrap_or_default()
391 } else {
392 dt
393 };
394 Ok(format_normal_dt(effective_dt, frac, digits, opts))
395 }
396}
397
398fn format_normal_dt(dt: DateTime<Utc>, frac: u32, digits: usize, opts: FormatOptions) -> String {
399 let base = dt.format("%Y-%m-%dT%H:%M:%S");
400 if digits == 0 {
401 let zulu = if opts.include_zulu { "Z" } else { "" };
402 format!("{base}{zulu}")
403 } else {
404 let zulu = if opts.include_zulu { "Z" } else { "" };
405 format!("{base}.{:0width$}{zulu}", frac, width = digits)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn parse_basic_z() {
415 let t = Time::<UTC>::parse_rfc3339("2000-01-01T12:00:00Z").unwrap();
416 let s = t.format_rfc3339(FormatOptions::SECONDS);
417 assert_eq!(s, "2000-01-01T12:00:00Z");
418 }
419
420 #[test]
421 fn parse_with_milliseconds() {
422 let t = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56.789Z").unwrap();
423 let s = t.format_rfc3339(FormatOptions::milliseconds());
424 assert_eq!(s, "2024-06-15T12:34:56.789Z");
425 }
426
427 #[test]
428 fn parse_with_microseconds_within_chrono_bridge_precision() {
429 let t = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56.123456Z").unwrap();
433 let s = t.format_rfc3339(FormatOptions::microseconds());
434 assert!(s.starts_with("2024-06-15T12:34:56.1234"), "got {s}");
435 }
436
437 #[test]
438 fn parse_with_nanoseconds_within_chrono_bridge_precision() {
439 let t = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56.123456789Z").unwrap();
442 let s = t.format_rfc3339(FormatOptions::nanoseconds());
443 assert!(s.starts_with("2024-06-15T12:34:56.1234"), "got {s}");
444 assert_eq!(s.len(), "2024-06-15T12:34:56.123456789Z".len());
445 }
446
447 #[test]
448 fn parse_with_named_offset_normalizes_to_utc() {
449 let t = Time::<UTC>::parse_rfc3339("2024-06-15T14:34:56+02:00").unwrap();
450 let s = t.format_rfc3339(FormatOptions::SECONDS);
451 assert_eq!(s, "2024-06-15T12:34:56Z");
452 }
453
454 #[test]
455 fn format_leap_second_emits_colon_sixty() {
456 let t = Time::<UTC>::parse_rfc3339("2016-12-31T23:59:60Z").unwrap();
458 let s = t.format_rfc3339(FormatOptions::SECONDS);
459 assert_eq!(s, "2016-12-31T23:59:60Z");
460 }
461
462 #[test]
463 fn format_leap_second_with_fraction() {
464 let t = Time::<UTC>::parse_rfc3339("2016-12-31T23:59:60.500Z").unwrap();
466 let s = t.format_rfc3339(FormatOptions::milliseconds());
467 assert_eq!(s, "2016-12-31T23:59:60.500Z");
468 }
469
470 #[test]
471 fn reject_malformed_input() {
472 assert!(Time::<UTC>::parse_rfc3339("not a date").is_err());
473 assert!(Time::<UTC>::parse_rfc3339("2024-13-01T00:00:00Z").is_err());
474 assert!(Time::<UTC>::parse_rfc3339("2024-06-15T25:00:00Z").is_err());
475 }
476
477 #[test]
478 fn reject_empty_fraction() {
479 let result = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56.Z");
481 assert!(result.is_err(), "expected Err for empty fraction, got Ok");
482 }
483
484 #[test]
485 fn reject_more_than_nine_fractional_digits() {
486 let result = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56.1234567890Z");
488 assert!(result.is_err(), "expected Err for >9 fractional digits");
489 }
490
491 #[test]
492 fn round_trip_seconds_precision() {
493 for s in ["2000-01-01T00:00:00Z", "1999-12-31T23:59:59Z"] {
494 let t = Time::<UTC>::parse_rfc3339(s).unwrap();
495 let back = t.format_rfc3339(FormatOptions::SECONDS);
496 assert_eq!(back, s, "round trip mismatch for {s}");
497 }
498 }
499
500 #[test]
501 fn format_options_constants_are_consistent() {
502 assert_eq!(FormatOptions::SECONDS.subsecond_digits, 0);
503 assert_eq!(FormatOptions::milliseconds().subsecond_digits, 3);
504 assert_eq!(FormatOptions::microseconds().subsecond_digits, 6);
505 assert_eq!(FormatOptions::nanoseconds().subsecond_digits, 9);
506 }
507
508 #[test]
509 fn arbitrary_precision_digits_are_supported() {
510 let t = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56.123456789Z").unwrap();
511 let opts = FormatOptions {
512 subsecond_digits: 4,
513 precision: FormatPrecision::Truncate,
514 include_zulu: true,
515 };
516 let s = t.format_rfc3339(opts);
517 assert!(s.starts_with("2024-06-15T12:34:56.1234"), "got {s}");
519 }
520
521 #[test]
522 fn truncate_vs_round_differs_on_5() {
523 let t = Time::<UTC>::parse_rfc3339("2000-06-15T12:34:56.55Z").unwrap();
525 let trunc = FormatOptions {
526 subsecond_digits: 1,
527 precision: FormatPrecision::Truncate,
528 include_zulu: true,
529 };
530 let round = FormatOptions {
531 subsecond_digits: 1,
532 precision: FormatPrecision::RoundHalfToEven,
533 include_zulu: true,
534 };
535 let st = t.format_rfc3339(trunc);
536 let sr = t.format_rfc3339(round);
537 assert!(st.ends_with(".5Z"), "truncate got {st}");
538 assert!(sr.ends_with(".6Z"), "round-half-to-even got {sr}");
540 }
541
542 #[test]
543 fn omit_zulu_suffix() {
544 let t = Time::<UTC>::parse_rfc3339("2024-06-15T12:34:56Z").unwrap();
545 let opts = FormatOptions {
546 subsecond_digits: 0,
547 precision: FormatPrecision::Truncate,
548 include_zulu: false,
549 };
550 let s = t.format_rfc3339(opts);
551 assert_eq!(s, "2024-06-15T12:34:56");
552 }
553
554 #[test]
555 fn reject_invalid_leap_second_date() {
556 let result = Time::<UTC>::parse_rfc3339("2023-06-15T23:59:60Z");
558 assert!(
559 matches!(result, Err(ConversionError::InvalidLeapSecond)),
560 "expected InvalidLeapSecond, got {result:?}"
561 );
562 assert!(
564 Time::<UTC>::parse_rfc3339("2016-12-31T23:59:60Z").is_ok(),
565 "expected Ok for valid leap-second date"
566 );
567 }
568
569 #[test]
570 fn rounding_truncate_standard_digits() {
571 let t = Time::<UTC>::parse_rfc3339("2000-01-01T12:34:56.123Z").unwrap();
574 let opts = FormatOptions {
575 subsecond_digits: 0,
576 precision: FormatPrecision::Truncate,
577 include_zulu: true,
578 };
579 let s = t.format_rfc3339(opts);
580 assert_eq!(s, "2000-01-01T12:34:56Z");
581 }
582
583 #[test]
584 fn rounding_carry_into_next_second() {
585 let t = Time::<UTC>::parse_rfc3339("2000-01-01T12:34:56.999Z").unwrap();
587 let opts = FormatOptions {
588 subsecond_digits: 0,
589 precision: FormatPrecision::RoundHalfToEven,
590 include_zulu: true,
591 };
592 let s = t.format_rfc3339(opts);
593 assert_eq!(s, "2000-01-01T12:34:57Z", "got {s}");
595 }
596
597 #[test]
598 fn rounding_half_even_milliseconds() {
599 let t = Time::<UTC>::parse_rfc3339("2000-01-01T12:00:00.500000000Z").unwrap();
604 let opts = FormatOptions {
605 subsecond_digits: 3,
606 precision: FormatPrecision::RoundHalfToEven,
607 include_zulu: true,
608 };
609 let s = t.format_rfc3339(opts);
610 assert_eq!(s, "2000-01-01T12:00:00.500Z", "got {s}");
611 }
612
613 #[test]
614 fn round_subsecond_helper_truncate() {
615 assert_eq!(
616 round_subsecond(999_999_999, 3, FormatPrecision::Truncate),
617 (999, false)
618 );
619 assert_eq!(
620 round_subsecond(500_000_000, 3, FormatPrecision::Truncate),
621 (500, false)
622 );
623 assert_eq!(round_subsecond(0, 0, FormatPrecision::Truncate), (0, false));
624 }
625
626 #[test]
627 fn round_subsecond_helper_carry() {
628 let (v, carry) = round_subsecond(999_999_999, 0, FormatPrecision::RoundHalfToEven);
630 assert!(carry, "expected carry for 999_999_999 at 0 digits");
631 assert_eq!(v, 0);
632 }
633
634 #[test]
635 fn round_subsecond_helper_half_even() {
636 let (v, carry) = round_subsecond(500_000_000, 0, FormatPrecision::RoundHalfToEven);
638 assert!(
639 !carry,
640 "500_000_000 half-to-even at 0 digits: 0 is even, no carry"
641 );
642 assert_eq!(v, 0);
643 let (v9, carry9) = round_subsecond(999_999_999, 9, FormatPrecision::RoundHalfToEven);
645 assert!(!carry9);
646 assert_eq!(v9, 999_999_999);
647 }
648}