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