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