1use alloc::format;
2use alloc::string::ToString;
3use core::fmt;
4use core::str::FromStr;
5
6use crate::error::{Error, ErrorKind};
7use crate::event::Atom;
8use crate::ext::Extension;
9use crate::ext::known::{WellKnown, floor, impl_well_known, invalid, out_of_range, round};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct Date {
16 pub year: u16,
18 pub month: u8,
20 pub day: u8,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct Time {
29 pub hour: u8,
31 pub minute: u8,
33 pub second: u8,
35 pub nanosecond: u32,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum Offset {
44 Z,
46 Custom {
48 minutes: i16,
50 },
51}
52
53#[derive(Clone, Copy, PartialEq, Eq, Hash)]
82pub struct Datetime {
83 pub date: Option<Date>,
85 pub time: Option<Time>,
87 pub offset: Option<Offset>,
89}
90
91#[cfg_attr(
116 not(feature = "std"),
117 doc = "[`std::time::SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html"
118)]
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub struct Timestamp {
121 pub seconds: i64,
123 pub nanosecond: u32,
125}
126
127impl Date {
128 pub fn is_valid(&self) -> bool {
130 self.year <= 9999
131 && (1..=12).contains(&self.month)
132 && self.day >= 1
133 && self.day <= days_in_month(i64::from(self.year), self.month)
134 }
135}
136
137impl Time {
138 pub fn is_valid(&self) -> bool {
140 self.hour <= 23 && self.minute <= 59 && self.second <= 60 && self.nanosecond < 1_000_000_000
141 }
142}
143
144impl Offset {
145 pub fn minutes(&self) -> i16 {
147 match *self {
148 Offset::Z => 0,
149 Offset::Custom { minutes } => minutes,
150 }
151 }
152
153 pub fn is_valid(&self) -> bool {
155 self.minutes().unsigned_abs() < 24 * 60
156 }
157}
158
159impl Datetime {
160 pub fn is_valid(&self) -> bool {
166 match (self.date, self.time, self.offset) {
167 (None, None, _) => false,
168 (date, time, Some(offset)) => {
169 date.is_some_and(|x| x.is_valid())
170 && time.is_some_and(|x| x.is_valid())
171 && offset.is_valid()
172 }
173 (date, time, None) => {
174 date.is_none_or(|x| x.is_valid()) && time.is_none_or(|x| x.is_valid())
175 }
176 }
177 }
178
179 pub fn kind(&self) -> &'static str {
184 match (self.date, self.time, self.offset) {
185 (Some(_), Some(_), Some(_)) => "offset date-time",
186 (Some(_), Some(_), None) => "local date-time",
187 (Some(_), None, _) => "local date",
188 (None, Some(_), _) => "local time",
189 (None, None, _) => "invalid date-time",
190 }
191 }
192
193 pub(crate) fn unexpected_kind(&self, expected: &str) -> Error {
195 Error::new(
196 ErrorKind::InvalidType,
197 format!("unexpected {}, expected {}", self.kind(), expected),
198 )
199 }
200
201 #[cfg(any(feature = "chrono", feature = "time", feature = "jiff"))]
203 pub(crate) fn expect_local_date(&self) -> Result<Date, Error> {
204 match (self.date, self.time) {
205 (Some(date), None) => Ok(date),
206 _ => Err(self.unexpected_kind("local date")),
207 }
208 }
209
210 #[cfg(any(feature = "chrono", feature = "time", feature = "jiff"))]
212 pub(crate) fn expect_local_time(&self) -> Result<Time, Error> {
213 match (self.date, self.time) {
214 (None, Some(time)) => Ok(time),
215 _ => Err(self.unexpected_kind("local time")),
216 }
217 }
218
219 #[cfg(any(feature = "chrono", feature = "time", feature = "jiff"))]
221 pub(crate) fn expect_local_datetime(&self) -> Result<(Date, Time), Error> {
222 match (self.date, self.time, self.offset) {
223 (Some(date), Some(time), None) => Ok((date, time)),
224 _ => Err(self.unexpected_kind("local date-time")),
225 }
226 }
227
228 pub(crate) fn expect_offset_datetime(&self) -> Result<(Date, Time, Offset), Error> {
230 match (self.date, self.time, self.offset) {
231 (Some(date), Some(time), Some(offset)) => Ok((date, time, offset)),
232 _ => Err(self.unexpected_kind("offset date-time")),
233 }
234 }
235}
236
237impl Timestamp {
238 #[cfg(feature = "std")]
241 pub(crate) fn normalized(seconds: i64, nanosecond: i64) -> Option<Timestamp> {
242 let seconds = seconds.checked_add(nanosecond.div_euclid(1_000_000_000))?;
243 Some(Timestamp {
244 seconds,
245 nanosecond: nanosecond.rem_euclid(1_000_000_000) as u32,
246 })
247 }
248
249 pub fn as_secs_f64(&self) -> f64 {
251 self.seconds as f64 + f64::from(self.nanosecond) / 1e9
252 }
253
254 pub fn to_datetime(&self) -> Option<Datetime> {
258 let (year, date, time) = self.to_civil();
259 Some(Datetime {
260 date: Some(Date {
261 year: u16::try_from(year).ok().filter(|&x| x <= 9999)?,
262 month: date.0,
263 day: date.1,
264 }),
265 time: Some(time),
266 offset: Some(Offset::Z),
267 })
268 }
269
270 fn to_civil(self) -> (i64, (u8, u8), Time) {
272 let days = self.seconds.div_euclid(86400);
273 let secs = self.seconds.rem_euclid(86400);
274 let (year, month, day) = civil_from_days(days);
275 (
276 year,
277 (month, day),
278 Time {
279 hour: (secs / 3600) as u8,
280 minute: (secs / 60 % 60) as u8,
281 second: (secs % 60) as u8,
282 nanosecond: self.nanosecond,
283 },
284 )
285 }
286
287 fn from_civil(year: i64, date: Date, time: Time, offset: Offset) -> Option<Timestamp> {
289 let days = days_from_civil(year, date.month, date.day);
290 let seconds = days.checked_mul(86400)?.checked_add(
291 i64::from(time.hour) * 3600
292 + i64::from(time.minute) * 60
293 + i64::from(time.second.min(59))
295 - i64::from(offset.minutes()) * 60,
296 )?;
297 Some(Timestamp {
298 seconds,
299 nanosecond: time.nanosecond,
300 })
301 }
302}
303
304impl TryFrom<Datetime> for Timestamp {
305 type Error = Error;
306
307 fn try_from(value: Datetime) -> Result<Timestamp, Error> {
311 let (date, time, offset) = value.expect_offset_datetime()?;
312 if !value.is_valid() {
313 return Err(invalid("invalid date-time"));
314 }
315 Timestamp::from_civil(i64::from(date.year), date, time, offset)
316 .ok_or_else(|| out_of_range("date-time out of range"))
317 }
318}
319
320impl TryFrom<Timestamp> for Datetime {
321 type Error = Error;
322
323 fn try_from(value: Timestamp) -> Result<Datetime, Error> {
325 value
326 .to_datetime()
327 .ok_or_else(|| out_of_range("timestamp out of range for date-time"))
328 }
329}
330
331impl From<Date> for Datetime {
332 fn from(date: Date) -> Datetime {
333 Datetime {
334 date: Some(date),
335 time: None,
336 offset: None,
337 }
338 }
339}
340
341impl From<Time> for Datetime {
342 fn from(time: Time) -> Datetime {
343 Datetime {
344 date: None,
345 time: Some(time),
346 offset: None,
347 }
348 }
349}
350
351pub(crate) fn is_leap_year(year: i64) -> bool {
352 year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
353}
354
355pub(crate) fn days_in_month(year: i64, month: u8) -> u8 {
356 match month {
357 2 if is_leap_year(year) => 29,
358 2 => 28,
359 4 | 6 | 9 | 11 => 30,
360 _ => 31,
361 }
362}
363
364pub(crate) fn days_from_civil(year: i64, month: u8, day: u8) -> i64 {
368 let (month, day) = (i64::from(month), i64::from(day));
369 let year = if month <= 2 { year - 1 } else { year };
370 let era = year.div_euclid(400);
371 let yoe = year - era * 400;
372 let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
373 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
374 era * 146097 + doe - 719468
375}
376
377pub(crate) fn civil_from_days(days: i64) -> (i64, u8, u8) {
379 let z = days + 719468;
380 let era = z.div_euclid(146097);
381 let doe = z - era * 146097;
382 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
383 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
384 let mp = (5 * doy + 2) / 153;
385 let day = (doy - (153 * mp + 2) / 5 + 1) as u8;
386 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
387 let year = yoe + era * 400 + i64::from(month <= 2);
388 (year, month, day)
389}
390
391impl fmt::Display for Date {
392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
394 }
395}
396
397impl fmt::Display for Time {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
400 write_fraction(f, self.nanosecond)
401 }
402}
403
404pub(crate) fn write_fraction(f: &mut fmt::Formatter<'_>, nanosecond: u32) -> fmt::Result {
406 if nanosecond != 0 {
407 let mut digits = 9;
408 let mut value = nanosecond;
409 while value.is_multiple_of(10) {
410 value /= 10;
411 digits -= 1;
412 }
413 write!(f, ".{:0width$}", value, width = digits)?;
414 }
415 Ok(())
416}
417
418impl fmt::Display for Offset {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 match *self {
421 Offset::Z => f.write_str("Z"),
422 Offset::Custom { minutes } => {
423 let sign = if minutes < 0 { '-' } else { '+' };
424 let minutes = minutes.unsigned_abs();
425 write!(f, "{}{:02}:{:02}", sign, minutes / 60, minutes % 60)
426 }
427 }
428 }
429}
430
431impl fmt::Display for Datetime {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 if let Some(ref date) = self.date {
434 write!(f, "{}", date)?;
435 if self.time.is_some() {
436 f.write_str("T")?;
437 }
438 }
439 if let Some(ref time) = self.time {
440 write!(f, "{}", time)?;
441 }
442 if let Some(ref offset) = self.offset {
443 write!(f, "{}", offset)?;
444 }
445 Ok(())
446 }
447}
448
449impl fmt::Debug for Datetime {
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 write!(f, "Datetime({})", self)
452 }
453}
454
455impl fmt::Display for Timestamp {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 let (year, (month, day), time) = self.to_civil();
458 if (0..=9999).contains(&year) {
459 write!(f, "{:04}", year)?;
460 } else {
461 write!(
463 f,
464 "{}{:06}",
465 if year < 0 { '-' } else { '+' },
466 year.unsigned_abs()
467 )?;
468 }
469 write!(f, "-{:02}-{:02}T{}Z", month, day, time)
470 }
471}
472
473impl FromStr for Datetime {
474 type Err = Error;
475
476 fn from_str(s: &str) -> Result<Datetime, Error> {
477 let (year, dt) = parse_datetime(s, false)?;
478 debug_assert!(year.is_none());
479 Ok(dt)
480 }
481}
482
483impl FromStr for Timestamp {
484 type Err = Error;
485
486 fn from_str(s: &str) -> Result<Timestamp, Error> {
491 match parse_datetime(s, true)? {
492 (Some(year), dt) => {
493 let (date, time, offset) = dt.expect_offset_datetime()?;
494 Timestamp::from_civil(year, date, time, offset)
495 .ok_or_else(|| out_of_range("timestamp out of range"))
496 }
497 (None, dt) => Timestamp::try_from(dt),
498 }
499 }
500}
501
502fn parse_datetime(s: &str, expanded_years: bool) -> Result<(Option<i64>, Datetime), Error> {
508 let bytes = s.as_bytes();
509 let mut pos = 0;
510 let mut rv = Datetime {
511 date: None,
512 time: None,
513 offset: None,
514 };
515 let mut expanded_year = None;
516
517 let has_date = if expanded_years && matches!(bytes.first(), Some(b'+' | b'-')) {
518 let digits = bytes[1..].iter().take_while(|x| x.is_ascii_digit()).count();
519 if digits < 6 {
520 return Err(invalid("invalid expanded year"));
521 }
522 let year: i64 = s[1..1 + digits]
523 .parse()
524 .map_err(|_| out_of_range("year out of range"))?;
525 expanded_year = Some(if bytes[0] == b'-' { -year } else { year });
526 pos = 1 + digits;
527 true
528 } else {
529 bytes.get(4) == Some(&b'-')
530 };
531
532 if has_date {
533 let year = match expanded_year {
534 Some(_) => 2000,
535 None => digits(bytes, &mut pos, 4)? as u16,
536 };
537 expect(bytes, &mut pos, b'-')?;
538 let month = digits(bytes, &mut pos, 2)? as u8;
539 expect(bytes, &mut pos, b'-')?;
540 let day = digits(bytes, &mut pos, 2)? as u8;
541 let date = Date { year, month, day };
542 let valid_day = match expanded_year {
543 Some(year) => {
544 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
545 }
546 None => date.is_valid(),
547 };
548 if !valid_day {
549 return Err(invalid("invalid date"));
550 }
551 rv.date = Some(date);
552 match bytes.get(pos) {
553 Some(b'T' | b't' | b' ') => pos += 1,
554 None => return Ok((expanded_year, rv)),
555 _ => return Err(invalid("invalid date-time")),
556 }
557 }
558
559 let mut time = Time {
560 hour: digits(bytes, &mut pos, 2)? as u8,
561 minute: {
562 expect(bytes, &mut pos, b':')?;
563 digits(bytes, &mut pos, 2)? as u8
564 },
565 second: 0,
566 nanosecond: 0,
567 };
568 if bytes.get(pos) == Some(&b':') {
569 pos += 1;
570 time.second = digits(bytes, &mut pos, 2)? as u8;
571 if bytes.get(pos) == Some(&b'.') {
572 pos += 1;
573 let start = pos;
574 let mut nanos = 0u32;
575 while let Some(&c) = bytes.get(pos).filter(|x| x.is_ascii_digit()) {
576 if pos - start < 9 {
578 nanos = nanos * 10 + u32::from(c - b'0');
579 }
580 pos += 1;
581 }
582 if pos == start {
583 return Err(invalid("expected digits after decimal point"));
584 }
585 for _ in (pos - start)..9 {
586 nanos *= 10;
587 }
588 time.nanosecond = nanos;
589 }
590 }
591 if !time.is_valid() {
592 return Err(invalid("invalid time"));
593 }
594 rv.time = Some(time);
595
596 if rv.date.is_some() {
597 match bytes.get(pos) {
598 Some(b'Z' | b'z') => {
599 pos += 1;
600 rv.offset = Some(Offset::Z);
601 }
602 Some(&sign @ (b'+' | b'-')) => {
603 pos += 1;
604 let hours = digits(bytes, &mut pos, 2)?;
605 expect(bytes, &mut pos, b':')?;
606 let minutes = digits(bytes, &mut pos, 2)?;
607 if hours > 23 || minutes > 59 {
608 return Err(invalid("invalid offset"));
609 }
610 let minutes = (hours * 60 + minutes) as i16;
611 rv.offset = Some(Offset::Custom {
612 minutes: if sign == b'-' { -minutes } else { minutes },
613 });
614 }
615 _ => {}
616 }
617 }
618
619 if pos != bytes.len() {
620 return Err(invalid("unexpected characters after date-time"));
621 }
622 Ok((expanded_year, rv))
623}
624
625fn digits(bytes: &[u8], pos: &mut usize, n: usize) -> Result<u32, Error> {
626 let mut rv = 0;
627 for _ in 0..n {
628 match bytes.get(*pos) {
629 Some(&c) if c.is_ascii_digit() => rv = rv * 10 + u32::from(c - b'0'),
630 _ => return Err(invalid("invalid date-time")),
631 }
632 *pos += 1;
633 }
634 Ok(rv)
635}
636
637fn expect(bytes: &[u8], pos: &mut usize, c: u8) -> Result<(), Error> {
638 if bytes.get(*pos) == Some(&c) {
639 *pos += 1;
640 Ok(())
641 } else {
642 Err(invalid("invalid date-time"))
643 }
644}
645
646impl Extension for Datetime {
647 fn name(&self) -> &str {
648 "datetime"
649 }
650
651 fn fallback(&self) -> Atom<'_> {
652 Atom::Str(self.to_string().into())
653 }
654}
655
656impl WellKnown for Datetime {
657 const EXPECTING: &'static str = "datetime";
658
659 fn from_atom(atom: &Atom) -> Result<Option<Datetime>, Error> {
661 Ok(Some(match *atom {
662 Atom::Ext(ref ext) => {
663 if let Some(value) = ext.downcast_ref::<Datetime>() {
664 *value
665 } else if let Some(value) = ext.downcast_ref::<Timestamp>() {
666 Datetime::try_from(*value)?
667 } else {
668 return Ok(None);
669 }
670 }
671 Atom::Str(ref value) => value.parse()?,
672 _ => return Ok(None),
673 }))
674 }
675}
676
677impl_well_known!(Datetime);
678
679impl Extension for Timestamp {
680 fn name(&self) -> &str {
681 "timestamp"
682 }
683
684 fn fallback(&self) -> Atom<'_> {
685 Atom::Str(self.to_string().into())
686 }
687}
688
689impl WellKnown for Timestamp {
690 const EXPECTING: &'static str = "timestamp";
691
692 fn from_atom(atom: &Atom) -> Result<Option<Timestamp>, Error> {
695 Ok(Some(match *atom {
696 Atom::Ext(ref ext) => {
697 if let Some(value) = ext.downcast_ref::<Timestamp>() {
698 *value
699 } else if let Some(value) = ext.downcast_ref::<Datetime>() {
700 Timestamp::try_from(*value)?
701 } else {
702 return Ok(None);
703 }
704 }
705 Atom::Str(ref value) => value.parse()?,
706 Atom::U64(value) => Timestamp {
707 seconds: i64::try_from(value)
708 .map_err(|_| out_of_range("timestamp out of range"))?,
709 nanosecond: 0,
710 },
711 Atom::I64(value) => Timestamp {
712 seconds: value,
713 nanosecond: 0,
714 },
715 Atom::F64(value) => {
716 if !value.is_finite() || value.abs() >= 9.2e18 {
717 return Err(out_of_range("timestamp out of range"));
718 }
719 let seconds = floor(value);
720 Timestamp {
721 seconds: seconds as i64,
722 nanosecond: (round((value - seconds) * 1e9) as u32).min(999_999_999),
723 }
724 }
725 _ => return Ok(None),
726 }))
727 }
728}
729
730impl_well_known!(Timestamp);
731
732#[test]
733fn test_civil_roundtrip() {
734 for days in [
735 -1_000_000i64,
736 -719468,
737 -1,
738 0,
739 1,
740 11016,
741 2_932_896,
742 10_000_000,
743 ] {
744 let (y, m, d) = civil_from_days(days);
745 assert_eq!(days_from_civil(y, m, d), days);
746 }
747 assert_eq!(civil_from_days(0), (1970, 1, 1));
748 assert_eq!(days_from_civil(2000, 3, 1), 11017);
749}
750
751#[test]
752fn test_timestamp_format() {
753 let ts = Timestamp {
754 seconds: -1,
755 nanosecond: 500_000_000,
756 };
757 assert_eq!(ts.to_string(), "1969-12-31T23:59:59.5Z");
758 assert_eq!(ts.to_string().parse::<Timestamp>().unwrap(), ts);
759 let far = Timestamp {
760 seconds: 253402300800,
761 nanosecond: 0,
762 };
763 assert_eq!(far.to_string(), "+010000-01-01T00:00:00Z");
764 assert_eq!(far.to_string().parse::<Timestamp>().unwrap(), far);
765 let before = Timestamp {
766 seconds: -62167219201,
767 nanosecond: 0,
768 };
769 assert_eq!(before.to_string(), "-000001-12-31T23:59:59Z");
770 assert_eq!(before.to_string().parse::<Timestamp>().unwrap(), before);
771 assert!("1979-05-27T07:32:00".parse::<Timestamp>().is_err());
772 assert!("+10000-01-01T00:00:00Z".parse::<Timestamp>().is_err());
773}