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 = i64::try_from(
293 days * 86400
294 + i128::from(time.hour) * 3600
295 + i128::from(time.minute) * 60
296 + i128::from(time.second.min(59))
298 - i128::from(offset.minutes()) * 60,
299 )
300 .ok()?;
301 Some(Timestamp {
302 seconds,
303 nanosecond: time.nanosecond,
304 })
305 }
306}
307
308impl TryFrom<Datetime> for Timestamp {
309 type Error = Error;
310
311 fn try_from(value: Datetime) -> Result<Timestamp, Error> {
315 let (date, time, offset) = value.expect_offset_datetime()?;
316 if !value.is_valid() {
317 return Err(invalid("invalid date-time"));
318 }
319 Timestamp::from_civil(i64::from(date.year), date, time, offset)
320 .ok_or_else(|| out_of_range("date-time out of range"))
321 }
322}
323
324impl TryFrom<Timestamp> for Datetime {
325 type Error = Error;
326
327 fn try_from(value: Timestamp) -> Result<Datetime, Error> {
329 value
330 .to_datetime()
331 .ok_or_else(|| out_of_range("timestamp out of range for date-time"))
332 }
333}
334
335impl From<Date> for Datetime {
336 fn from(date: Date) -> Datetime {
337 Datetime {
338 date: Some(date),
339 time: None,
340 offset: None,
341 }
342 }
343}
344
345impl From<Time> for Datetime {
346 fn from(time: Time) -> Datetime {
347 Datetime {
348 date: None,
349 time: Some(time),
350 offset: None,
351 }
352 }
353}
354
355pub(crate) fn is_leap_year(year: i64) -> bool {
356 year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
357}
358
359pub(crate) fn days_in_month(year: i64, month: u8) -> u8 {
360 match month {
361 2 if is_leap_year(year) => 29,
362 2 => 28,
363 4 | 6 | 9 | 11 => 30,
364 _ => 31,
365 }
366}
367
368pub(crate) fn days_from_civil(year: i64, month: u8, day: u8) -> i128 {
372 let year = i128::from(year);
374 let (month, day) = (i128::from(month), i128::from(day));
375 let year = if month <= 2 { year - 1 } else { year };
376 let era = year.div_euclid(400);
377 let yoe = year - era * 400;
378 let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
379 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
380 era * 146097 + doe - 719468
381}
382
383pub(crate) fn civil_from_days(days: i64) -> (i64, u8, u8) {
385 let z = days + 719468;
386 let era = z.div_euclid(146097);
387 let doe = z - era * 146097;
388 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
389 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
390 let mp = (5 * doy + 2) / 153;
391 let day = (doy - (153 * mp + 2) / 5 + 1) as u8;
392 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
393 let year = yoe + era * 400 + i64::from(month <= 2);
394 (year, month, day)
395}
396
397impl fmt::Display for Date {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
400 }
401}
402
403impl fmt::Display for Time {
404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
406 write_fraction(f, self.nanosecond)
407 }
408}
409
410pub(crate) fn write_fraction(f: &mut fmt::Formatter<'_>, nanosecond: u32) -> fmt::Result {
412 if nanosecond != 0 {
413 let mut digits = 9;
414 let mut value = nanosecond;
415 while value.is_multiple_of(10) {
416 value /= 10;
417 digits -= 1;
418 }
419 write!(f, ".{:0width$}", value, width = digits)?;
420 }
421 Ok(())
422}
423
424impl fmt::Display for Offset {
425 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426 match *self {
427 Offset::Z => f.write_str("Z"),
428 Offset::Custom { minutes } => {
429 let sign = if minutes < 0 { '-' } else { '+' };
430 let minutes = minutes.unsigned_abs();
431 write!(f, "{}{:02}:{:02}", sign, minutes / 60, minutes % 60)
432 }
433 }
434 }
435}
436
437impl fmt::Display for Datetime {
438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439 if let Some(ref date) = self.date {
440 write!(f, "{}", date)?;
441 if self.time.is_some() {
442 f.write_str("T")?;
443 }
444 }
445 if let Some(ref time) = self.time {
446 write!(f, "{}", time)?;
447 }
448 if let Some(ref offset) = self.offset {
449 write!(f, "{}", offset)?;
450 }
451 Ok(())
452 }
453}
454
455impl fmt::Debug for Datetime {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 write!(f, "Datetime({})", self)
458 }
459}
460
461impl fmt::Display for Timestamp {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 let (year, (month, day), time) = self.to_civil();
464 if (0..=9999).contains(&year) {
465 write!(f, "{:04}", year)?;
466 } else {
467 write!(
469 f,
470 "{}{:06}",
471 if year < 0 { '-' } else { '+' },
472 year.unsigned_abs()
473 )?;
474 }
475 write!(f, "-{:02}-{:02}T{}Z", month, day, time)
476 }
477}
478
479impl FromStr for Datetime {
480 type Err = Error;
481
482 fn from_str(s: &str) -> Result<Datetime, Error> {
483 let (year, dt) = parse_datetime(s, false)?;
484 debug_assert!(year.is_none());
485 Ok(dt)
486 }
487}
488
489impl FromStr for Timestamp {
490 type Err = Error;
491
492 fn from_str(s: &str) -> Result<Timestamp, Error> {
497 match parse_datetime(s, true)? {
498 (Some(year), dt) => {
499 let (date, time, offset) = dt.expect_offset_datetime()?;
500 Timestamp::from_civil(year, date, time, offset)
501 .ok_or_else(|| out_of_range("timestamp out of range"))
502 }
503 (None, dt) => Timestamp::try_from(dt),
504 }
505 }
506}
507
508fn parse_datetime(s: &str, expanded_years: bool) -> Result<(Option<i64>, Datetime), Error> {
514 let bytes = s.as_bytes();
515 let mut pos = 0;
516 let mut rv = Datetime {
517 date: None,
518 time: None,
519 offset: None,
520 };
521 let mut expanded_year = None;
522
523 let has_date = if expanded_years && matches!(bytes.first(), Some(b'+' | b'-')) {
524 let digits = bytes[1..].iter().take_while(|x| x.is_ascii_digit()).count();
525 if digits < 6 {
526 return Err(invalid("invalid expanded year"));
527 }
528 let year: i64 = s[1..1 + digits]
529 .parse()
530 .map_err(|_| out_of_range("year out of range"))?;
531 expanded_year = Some(if bytes[0] == b'-' { -year } else { year });
532 pos = 1 + digits;
533 true
534 } else {
535 bytes.get(4) == Some(&b'-')
536 };
537
538 if has_date {
539 let year = match expanded_year {
540 Some(_) => 2000,
541 None => digits(bytes, &mut pos, 4)? as u16,
542 };
543 expect(bytes, &mut pos, b'-')?;
544 let month = digits(bytes, &mut pos, 2)? as u8;
545 expect(bytes, &mut pos, b'-')?;
546 let day = digits(bytes, &mut pos, 2)? as u8;
547 let date = Date { year, month, day };
548 let valid_day = match expanded_year {
549 Some(year) => {
550 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
551 }
552 None => date.is_valid(),
553 };
554 if !valid_day {
555 return Err(invalid("invalid date"));
556 }
557 rv.date = Some(date);
558 match bytes.get(pos) {
559 Some(b'T' | b't' | b' ') => pos += 1,
560 None => return Ok((expanded_year, rv)),
561 _ => return Err(invalid("invalid date-time")),
562 }
563 }
564
565 let mut time = Time {
566 hour: digits(bytes, &mut pos, 2)? as u8,
567 minute: {
568 expect(bytes, &mut pos, b':')?;
569 digits(bytes, &mut pos, 2)? as u8
570 },
571 second: 0,
572 nanosecond: 0,
573 };
574 if bytes.get(pos) == Some(&b':') {
575 pos += 1;
576 time.second = digits(bytes, &mut pos, 2)? as u8;
577 if bytes.get(pos) == Some(&b'.') {
578 pos += 1;
579 let start = pos;
580 let mut nanos = 0u32;
581 while let Some(&c) = bytes.get(pos).filter(|x| x.is_ascii_digit()) {
582 if pos - start < 9 {
584 nanos = nanos * 10 + u32::from(c - b'0');
585 }
586 pos += 1;
587 }
588 if pos == start {
589 return Err(invalid("expected digits after decimal point"));
590 }
591 for _ in (pos - start)..9 {
592 nanos *= 10;
593 }
594 time.nanosecond = nanos;
595 }
596 }
597 if !time.is_valid() {
598 return Err(invalid("invalid time"));
599 }
600 rv.time = Some(time);
601
602 if rv.date.is_some() {
603 match bytes.get(pos) {
604 Some(b'Z' | b'z') => {
605 pos += 1;
606 rv.offset = Some(Offset::Z);
607 }
608 Some(&sign @ (b'+' | b'-')) => {
609 pos += 1;
610 let hours = digits(bytes, &mut pos, 2)?;
611 expect(bytes, &mut pos, b':')?;
612 let minutes = digits(bytes, &mut pos, 2)?;
613 if hours > 23 || minutes > 59 {
614 return Err(invalid("invalid offset"));
615 }
616 let minutes = (hours * 60 + minutes) as i16;
617 rv.offset = Some(Offset::Custom {
618 minutes: if sign == b'-' { -minutes } else { minutes },
619 });
620 }
621 _ => {}
622 }
623 }
624
625 if pos != bytes.len() {
626 return Err(invalid("unexpected characters after date-time"));
627 }
628 Ok((expanded_year, rv))
629}
630
631fn digits(bytes: &[u8], pos: &mut usize, n: usize) -> Result<u32, Error> {
632 let mut rv = 0;
633 for _ in 0..n {
634 match bytes.get(*pos) {
635 Some(&c) if c.is_ascii_digit() => rv = rv * 10 + u32::from(c - b'0'),
636 _ => return Err(invalid("invalid date-time")),
637 }
638 *pos += 1;
639 }
640 Ok(rv)
641}
642
643fn expect(bytes: &[u8], pos: &mut usize, c: u8) -> Result<(), Error> {
644 if bytes.get(*pos) == Some(&c) {
645 *pos += 1;
646 Ok(())
647 } else {
648 Err(invalid("invalid date-time"))
649 }
650}
651
652impl Extension for Datetime {
653 fn name(&self) -> &str {
654 "datetime"
655 }
656
657 fn fallback(&self) -> Atom<'_> {
658 Atom::Str(self.to_string().into())
659 }
660}
661
662impl WellKnown for Datetime {
663 const EXPECTING: &'static str = "datetime";
664
665 fn from_atom(atom: &Atom) -> Result<Option<Datetime>, Error> {
667 Ok(Some(match *atom {
668 Atom::Ext(ref ext) => {
669 if let Some(value) = ext.downcast_ref::<Datetime>() {
670 *value
671 } else if let Some(value) = ext.downcast_ref::<Timestamp>() {
672 Datetime::try_from(*value)?
673 } else {
674 return Ok(None);
675 }
676 }
677 Atom::Str(ref value) => value.parse()?,
678 _ => return Ok(None),
679 }))
680 }
681}
682
683impl_well_known!(Datetime);
684
685impl Extension for Timestamp {
686 fn name(&self) -> &str {
687 "timestamp"
688 }
689
690 fn fallback(&self) -> Atom<'_> {
691 Atom::Str(self.to_string().into())
692 }
693}
694
695impl WellKnown for Timestamp {
696 const EXPECTING: &'static str = "timestamp";
697
698 fn from_atom(atom: &Atom) -> Result<Option<Timestamp>, Error> {
701 Ok(Some(match *atom {
702 Atom::Ext(ref ext) => {
703 if let Some(value) = ext.downcast_ref::<Timestamp>() {
704 *value
705 } else if let Some(value) = ext.downcast_ref::<Datetime>() {
706 Timestamp::try_from(*value)?
707 } else {
708 return Ok(None);
709 }
710 }
711 Atom::Str(ref value) => value.parse()?,
712 Atom::U64(value) => Timestamp {
713 seconds: i64::try_from(value)
714 .map_err(|_| out_of_range("timestamp out of range"))?,
715 nanosecond: 0,
716 },
717 Atom::I64(value) => Timestamp {
718 seconds: value,
719 nanosecond: 0,
720 },
721 Atom::F64(value) => {
722 if !value.is_finite() || value.abs() >= 9.2e18 {
723 return Err(out_of_range("timestamp out of range"));
724 }
725 let seconds = floor(value);
726 Timestamp {
727 seconds: seconds as i64,
728 nanosecond: (round((value - seconds) * 1e9) as u32).min(999_999_999),
729 }
730 }
731 _ => return Ok(None),
732 }))
733 }
734}
735
736impl_well_known!(Timestamp);
737
738#[test]
739fn test_civil_roundtrip() {
740 for days in [
741 -1_000_000i64,
742 -719468,
743 -1,
744 0,
745 1,
746 11016,
747 2_932_896,
748 10_000_000,
749 ] {
750 let (y, m, d) = civil_from_days(days);
751 assert_eq!(days_from_civil(y, m, d), i128::from(days));
752 }
753 assert_eq!(civil_from_days(0), (1970, 1, 1));
754 assert_eq!(days_from_civil(2000, 3, 1), 11017);
755}
756
757#[test]
758fn test_timestamp_format() {
759 let ts = Timestamp {
760 seconds: -1,
761 nanosecond: 500_000_000,
762 };
763 assert_eq!(ts.to_string(), "1969-12-31T23:59:59.5Z");
764 assert_eq!(ts.to_string().parse::<Timestamp>().unwrap(), ts);
765 let far = Timestamp {
766 seconds: 253402300800,
767 nanosecond: 0,
768 };
769 assert_eq!(far.to_string(), "+010000-01-01T00:00:00Z");
770 assert_eq!(far.to_string().parse::<Timestamp>().unwrap(), far);
771 let before = Timestamp {
772 seconds: -62167219201,
773 nanosecond: 0,
774 };
775 assert_eq!(before.to_string(), "-000001-12-31T23:59:59Z");
776 assert_eq!(before.to_string().parse::<Timestamp>().unwrap(), before);
777 assert!("1979-05-27T07:32:00".parse::<Timestamp>().is_err());
778 assert!("+10000-01-01T00:00:00Z".parse::<Timestamp>().is_err());
779}