1#![deny(clippy::all)]
62#![warn(clippy::pedantic)]
63#![warn(clippy::cargo)]
64#![warn(missing_docs)]
65
66use {
67 core::fmt::{self, Display},
68 sha1::{Digest, Sha1},
69 std::{io::BufRead, time::SystemTime},
70};
71
72pub use errors::*;
73
74pub mod errors;
75
76const SECONDS_PER_MINUTE: u8 = 60;
77const MINUTES_PER_HOUR: u8 = 60;
78const HOURS_PER_DAY: u8 = 24;
79
80const SECONDS_PER_HOUR: u64 = (MINUTES_PER_HOUR as u64) * (SECONDS_PER_MINUTE as u64);
81const SECONDS_PER_DAY: u64 = (HOURS_PER_DAY as u64) * SECONDS_PER_HOUR;
82
83const JANUARY: u8 = 1;
84const FEBRUARY: u8 = 2;
85const MARCH: u8 = 3;
86const APRIL: u8 = 4;
87const MAY: u8 = 5;
88const JUNE: u8 = 6;
89const JULY: u8 = 7;
90const AUGUST: u8 = 8;
91const SEPTEMBER: u8 = 9;
92const OCTOBER: u8 = 10;
93const NOVEMBER: u8 = 11;
94const DECEMBER: u8 = 12;
95
96const DAYS_PER_ERA: u64 = 365 * 400 + 100 - 4 + 1;
97const DAYS_BETWEEN_1900_01_01_AND_0000_03_01: u64 =
98 1900 * 365 + (1900 / 400) - (1900 / 100) + (1900 / 4) - 31 - 28;
99
100#[allow(clippy::identity_op)]
101const DAYS_BETWEEN_1900_01_01_AND_1970_01_01: u64 = 70 * 365 + (70 / 400) - (70 / 100) + (70 / 4);
102const SECONDS_BETWEEN_1900_01_01_AND_1970_01_01: u64 =
103 DAYS_BETWEEN_1900_01_01_AND_1970_01_01 * SECONDS_PER_DAY;
104
105#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
109pub struct Date {
110 year: u64,
111 month: u8,
112 day: u8,
113}
114
115impl Date {
116 pub const fn new(year: u64, month: u8, day: u8) -> Result<Self, InvalidDate> {
138 if month > 12 || month < 1 {
139 Err(InvalidDate::MonthOutOfRange(month))
140 } else if day < 1 || day > days_in_month(month, year) {
141 Err(InvalidDate::DayOutOfRange(day))
142 } else {
143 Ok(Date { year, month, day })
144 }
145 }
146
147 #[must_use]
158 pub const fn day(self) -> u8 {
159 self.day
160 }
161
162 #[must_use]
173 pub const fn month(self) -> u8 {
174 self.month
175 }
176
177 #[must_use]
188 pub const fn year(self) -> u64 {
189 self.year
190 }
191
192 const fn days_since_1900(self) -> u64 {
193 let month = self.month as u64;
197 let day = self.day as u64;
198
199 let year = self.year - (month <= 2) as u64;
200 let era = year / 400;
201 let year_of_era = year - era * 400; let day_of_year = (153 * ((month + 9) % 12) + 2) / 5 + day - 1; let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; era * DAYS_PER_ERA + day_of_era - DAYS_BETWEEN_1900_01_01_AND_0000_03_01
206 }
207}
208
209#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
211pub struct Time {
212 hours: u8,
213 minutes: u8,
214 seconds: u8,
215}
216
217impl Time {
218 pub fn new(hours: u8, minutes: u8, seconds: u8) -> Result<Self, InvalidTime> {
240 if hours >= HOURS_PER_DAY {
241 Err(InvalidTime::HoursOutOfRange(hours))
242 } else if minutes >= MINUTES_PER_HOUR {
243 Err(InvalidTime::MinutesOutOfRange(minutes))
244 } else if seconds >= SECONDS_PER_MINUTE {
245 Err(InvalidTime::SecondsOutOfRange(seconds))
246 } else {
247 Ok(Time {
248 hours,
249 minutes,
250 seconds,
251 })
252 }
253 }
254
255 #[must_use]
266 pub const fn hours(self) -> u8 {
267 self.hours
268 }
269
270 #[must_use]
281 pub const fn minutes(self) -> u8 {
282 self.minutes
283 }
284
285 #[must_use]
296 pub const fn seconds(self) -> u8 {
297 self.seconds
298 }
299
300 const fn total_seconds(self) -> u64 {
301 (self.hours as u64) * SECONDS_PER_HOUR
302 + (self.minutes as u64) * (SECONDS_PER_MINUTE as u64)
303 + (self.seconds as u64)
304 }
305}
306
307#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
311pub struct DateTime {
312 pub date: Date,
314 pub time: Time,
316}
317
318impl From<Timestamp> for DateTime {
319 fn from(timestamp: Timestamp) -> Self {
320 timestamp.date_time()
321 }
322}
323
324const fn is_leap_year(year: u64) -> bool {
325 (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))
326}
327
328const fn days_in_month(month: u8, year: u64) -> u8 {
329 match month {
330 JANUARY | MARCH | MAY | JULY | AUGUST | OCTOBER | DECEMBER => 31,
331 APRIL | JUNE | SEPTEMBER | NOVEMBER => 30,
332 FEBRUARY => {
333 if is_leap_year(year) {
334 29
335 } else {
336 28
337 }
338 }
339 _ => panic!("invalid month"),
342 }
343}
344
345#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
349pub struct Timestamp {
350 value: u64,
351}
352
353impl Timestamp {
354 pub const MAX_REPRESENTABLE_DATE_TIME: DateTime = Self::from_u64(u64::MAX).date_time();
373
374 pub const MIN_REPRESENTABLE_DATE_TIME: DateTime = Self::from_u64(0).date_time();
393
394 pub fn from_date_time(date_time: DateTime) -> Result<Self, DateTimeNotRepresentable> {
411 if (date_time >= Self::MIN_REPRESENTABLE_DATE_TIME)
412 && (date_time <= Self::MAX_REPRESENTABLE_DATE_TIME)
413 {
414 Ok(Timestamp::from_u64(
415 date_time.date.days_since_1900() * SECONDS_PER_DAY + date_time.time.total_seconds(),
416 ))
417 } else {
418 Err(DateTimeNotRepresentable { date_time })
419 }
420 }
421
422 #[must_use]
424 pub const fn from_u64(value: u64) -> Self {
425 Self { value }
426 }
427
428 #[must_use]
430 pub fn now() -> Self {
431 let secs_since_unix_epoch = SystemTime::now()
432 .duration_since(SystemTime::UNIX_EPOCH)
433 .expect("now is later than the unix epoch")
434 .as_secs();
435 let secs_since_1900_01_01 =
436 secs_since_unix_epoch + SECONDS_BETWEEN_1900_01_01_AND_1970_01_01;
437
438 Timestamp::from_u64(secs_since_1900_01_01)
439 }
440
441 #[must_use]
443 pub const fn as_u64(self) -> u64 {
444 self.value
445 }
446
447 #[must_use]
449 pub const fn date_time(self) -> DateTime {
450 DateTime {
451 date: self.date(),
452 time: self.time(),
453 }
454 }
455
456 #[must_use]
458 pub const fn time(self) -> Time {
459 Time {
460 hours: self.hours(),
461 minutes: self.minutes(),
462 seconds: self.seconds(),
463 }
464 }
465
466 #[must_use]
468 pub const fn date(self) -> Date {
469 let days_since_1900_01_01 = self.total_days();
473 let days_since_0000_03_01 = days_since_1900_01_01 + DAYS_BETWEEN_1900_01_01_AND_0000_03_01;
474 let era = days_since_0000_03_01 / DAYS_PER_ERA;
475 let day_of_era = days_since_0000_03_01 % DAYS_PER_ERA; let year_of_era =
477 (day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146_096) / 365; let year = year_of_era + era * 400;
479 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); let mp = (5 * day_of_year + 2) / 153; #[allow(clippy::cast_possible_truncation)]
483 let day = (day_of_year - (153 * mp + 2) / 5 + 1) as u8; let month = ((mp + 2) % 12) as u8 + 1; let year = year + (month <= 2) as u64;
486
487 Date { year, month, day }
488 }
489
490 const fn hours(self) -> u8 {
491 #[allow(clippy::cast_possible_truncation)]
492 {
493 (self.total_hours() % (HOURS_PER_DAY as u64)) as u8
494 }
495 }
496
497 const fn minutes(self) -> u8 {
498 #[allow(clippy::cast_possible_truncation)]
499 {
500 (self.total_minutes() % (MINUTES_PER_HOUR as u64)) as u8
501 }
502 }
503
504 const fn seconds(self) -> u8 {
505 #[allow(clippy::cast_possible_truncation)]
506 {
507 (self.total_seconds() % (SECONDS_PER_MINUTE as u64)) as u8
508 }
509 }
510
511 const fn total_seconds(self) -> u64 {
512 self.value
513 }
514
515 const fn total_minutes(self) -> u64 {
516 self.value / (SECONDS_PER_MINUTE as u64)
517 }
518
519 const fn total_hours(self) -> u64 {
520 self.value / SECONDS_PER_HOUR
521 }
522
523 const fn total_days(self) -> u64 {
524 self.value / SECONDS_PER_DAY
525 }
526}
527
528impl Display for Timestamp {
529 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
530 write!(f, "{}", self.value)
531 }
532}
533
534impl From<u64> for Timestamp {
535 fn from(timestamp: u64) -> Timestamp {
536 Self::from_u64(timestamp)
537 }
538}
539
540impl From<Timestamp> for u64 {
541 fn from(timestamp: Timestamp) -> u64 {
542 timestamp.as_u64()
543 }
544}
545
546impl TryFrom<DateTime> for Timestamp {
547 type Error = DateTimeNotRepresentable;
548 fn try_from(date_time: DateTime) -> Result<Self, Self::Error> {
549 Self::from_date_time(date_time)
550 }
551}
552
553#[derive(Clone, Copy, Debug, Eq, PartialEq)]
567pub struct LeapSecond {
568 timestamp: Timestamp,
569 tai_diff: u16,
570}
571
572impl LeapSecond {
573 #[must_use]
575 pub const fn timestamp(self) -> Timestamp {
576 self.timestamp
577 }
578
579 #[must_use]
584 pub const fn tai_diff(self) -> u16 {
585 self.tai_diff
586 }
587}
588
589#[derive(Clone, Debug)]
590struct Line {
591 content: String,
592 number: usize,
593}
594
595impl Line {
596 fn kind(&self) -> LineType {
597 if self.content.starts_with('#') {
598 match self.content[1..].chars().next() {
599 Some('$') => LineType::LastUpdate,
600 Some('@') => LineType::ExpirationDate,
601 Some('h') => LineType::Hash,
602 _ => LineType::Comment,
603 }
604 } else {
605 LineType::LeapSecond
606 }
607 }
608}
609
610#[derive(Clone, Copy, Debug, Eq, PartialEq)]
611enum LineType {
612 Comment,
613 LastUpdate,
614 ExpirationDate,
615 LeapSecond,
616 Hash,
617}
618
619#[derive(Clone, Copy, Debug)]
620struct LineBorrow<'a> {
621 content: &'a str,
622 number: usize,
623}
624
625fn extract_content(line: &Line) -> LineBorrow<'_> {
626 LineBorrow {
627 content: line.content[2..].trim(),
628 number: line.number,
629 }
630}
631
632fn parse_timestamp(timestamp: LineBorrow<'_>) -> Result<Timestamp, ParseLineError> {
633 let timestamp = timestamp
634 .content
635 .parse::<u64>()
636 .map_err(|_| ParseLineError {
637 cause: ParseLineErrorKind::InvalidTimestamp,
638 line: timestamp.content.to_owned(),
639 line_number: timestamp.number,
640 })?;
641
642 Ok(Timestamp::from_u64(timestamp))
643}
644
645#[derive(Clone, Debug, Eq, PartialEq)]
647pub struct Sha1Hash {
648 bytes: [u8; 20],
649}
650
651impl Sha1Hash {
652 const fn from_bytes(array: [u8; 20]) -> Self {
653 Self { bytes: array }
654 }
655
656 #[must_use]
658 pub const fn as_bytes(&self) -> &[u8; 20] {
659 &self.bytes
660 }
661
662 #[must_use]
664 pub const fn into_bytes(self) -> [u8; 20] {
665 self.bytes
666 }
667}
668
669impl From<Sha1Hash> for [u8; 20] {
670 fn from(hash: Sha1Hash) -> Self {
671 hash.into_bytes()
672 }
673}
674
675impl Display for Sha1Hash {
676 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
677 let to_string = self
678 .bytes
679 .iter()
680 .map(|byte| format!("{byte:0>2x}"))
681 .collect::<String>();
682 write!(f, "{to_string}")
683 }
684}
685
686fn parse_hash(hash: LineBorrow) -> Result<Sha1Hash, ParseLineError> {
687 let hash_vec = hash
688 .content
689 .split_ascii_whitespace()
690 .map(|word| {
691 u32::from_str_radix(word, 16).map_err(|_| ParseLineError {
692 cause: ParseLineErrorKind::InvalidHash,
693 line: hash.content.to_owned(),
694 line_number: hash.number,
695 })
696 })
697 .collect::<Result<Vec<_>, _>>()?
698 .into_iter()
699 .flat_map(u32::to_be_bytes)
700 .collect::<Vec<_>>();
701
702 let hash = TryInto::<[u8; 20]>::try_into(hash_vec).map_err(|_| ParseLineError {
703 cause: ParseLineErrorKind::InvalidHash,
704 line: hash.content.to_owned(),
705 line_number: hash.number,
706 })?;
707
708 Ok(Sha1Hash::from_bytes(hash))
709}
710
711fn parse_leap_second_lines(
712 lines: &[Line],
713) -> Result<Vec<(LineBorrow<'_>, LineBorrow<'_>)>, ParseLineError> {
714 lines
715 .iter()
716 .map(|line| {
717 let mut leap_second = line.content.as_str();
718 if let Some(start_of_comment) = leap_second.find('#') {
719 leap_second = &leap_second[..start_of_comment];
720 }
721 let leap_second = leap_second.trim();
722
723 let leap_second = leap_second
724 .split(|c: char| c.is_ascii_whitespace())
725 .filter(|s| !s.is_empty())
726 .collect::<Vec<_>>();
727
728 if leap_second.len() == 2 {
729 Ok((
730 LineBorrow {
731 content: leap_second[0],
732 number: line.number,
733 },
734 LineBorrow {
735 content: leap_second[1],
736 number: line.number,
737 },
738 ))
739 } else {
740 Err(ParseLineError {
741 cause: ParseLineErrorKind::InvalidLeapSecondLine,
742 line: line.content.clone(),
743 line_number: line.number,
744 })
745 }
746 })
747 .collect::<Result<Vec<_>, _>>()
748}
749
750fn calculate_hash<'a>(
751 last_update: LineBorrow<'a>,
752 expiration_date: LineBorrow<'a>,
753 leap_seconds: &'a [(LineBorrow<'a>, LineBorrow<'a>)],
754) -> Sha1Hash {
755 let mut hasher = Sha1::new();
756
757 hasher.update(last_update.content.as_bytes());
758 hasher.update(expiration_date.content.as_bytes());
759
760 for chunk in leap_seconds.iter().flat_map(|(s1, s2)| [s1, s2]) {
761 hasher.update(chunk.content.as_bytes());
762 }
763
764 Sha1Hash::from_bytes(hasher.finalize().into())
765}
766
767fn parse_tai_diff(tai_diff: LineBorrow<'_>) -> Result<u16, ParseLineError> {
768 tai_diff.content.parse::<u16>().map_err(|_| ParseLineError {
769 cause: ParseLineErrorKind::InvalidTaiDiff,
770 line: tai_diff.content.to_owned(),
771 line_number: tai_diff.number,
772 })
773}
774
775fn parse_leap_seconds<'a>(
776 leap_second_lines: &[(LineBorrow<'a>, LineBorrow<'a>)],
777) -> Result<Vec<LeapSecond>, ParseLineError> {
778 let mut leap_seconds = leap_second_lines
779 .iter()
780 .map(|(timestamp, tai_diff)| {
781 Ok(LeapSecond {
782 timestamp: parse_timestamp(*timestamp)?,
783 tai_diff: parse_tai_diff(*tai_diff)?,
784 })
785 })
786 .collect::<Result<Vec<_>, _>>()?;
787
788 leap_seconds.sort_by(|t1, t2| t1.timestamp.cmp(&t2.timestamp));
789
790 Ok(leap_seconds)
791}
792
793fn set_option(
794 option: &Option<Line>,
795 to: Line,
796 data_component: DataComponent,
797) -> Result<Line, ParseFileError> {
798 if let Some(line) = option {
799 Err(ParseFileError::DuplicateData {
800 data_component,
801 line1: line.number,
802 line2: to.number,
803 })
804 } else {
805 Ok(to)
806 }
807}
808
809fn extract_content_lines<R: BufRead>(file: R) -> Result<ContentLines, ParseFileError> {
810 let mut last_update = None;
811 let mut expiration_date = None;
812 let mut leap_seconds = Vec::new();
813 let mut hash = None;
814
815 let lines = file
816 .lines()
817 .enumerate()
818 .map(|(number, line)| line.map(|content| Line { content, number }));
819
820 for line in lines {
821 let line = line?;
822 match line.kind() {
823 LineType::Comment => continue,
824 LineType::LeapSecond => leap_seconds.push(line),
825 LineType::LastUpdate => {
826 last_update = Some(set_option(&last_update, line, DataComponent::LastUpdate)?);
827 }
828 LineType::ExpirationDate => {
829 expiration_date = Some(set_option(
830 &expiration_date,
831 line,
832 DataComponent::ExpirationDate,
833 )?);
834 }
835 LineType::Hash => {
836 hash = Some(set_option(&hash, line, DataComponent::Hash)?);
837 }
838 }
839 }
840
841 let last_update = last_update.ok_or(ParseFileError::MissingData(DataComponent::LastUpdate))?;
842 let expiration_date =
843 expiration_date.ok_or(ParseFileError::MissingData(DataComponent::ExpirationDate))?;
844 let hash = hash.ok_or(ParseFileError::MissingData(DataComponent::Hash))?;
845
846 Ok(ContentLines {
847 last_update,
848 expiration_date,
849 hash,
850 leap_seconds,
851 })
852}
853
854#[derive(Clone, Debug)]
855struct ContentLines {
856 last_update: Line,
857 expiration_date: Line,
858 hash: Line,
859 leap_seconds: Vec<Line>,
860}
861
862impl ContentLines {
863 fn last_update(&self) -> LineBorrow<'_> {
864 extract_content(&self.last_update)
865 }
866
867 fn expiration_date(&self) -> LineBorrow<'_> {
868 extract_content(&self.expiration_date)
869 }
870
871 fn hash(&self) -> LineBorrow<'_> {
872 extract_content(&self.hash)
873 }
874}
875
876#[derive(Clone, Debug, Eq, PartialEq)]
883pub struct LeapSecondsList {
884 last_update: Timestamp,
885 expiration_date: Timestamp,
886 leap_seconds: Vec<LeapSecond>,
887}
888
889impl LeapSecondsList {
890 pub fn new<R: BufRead>(file: R) -> Result<Self, ParseFileError> {
898 let content_lines = extract_content_lines(file)?;
899
900 let last_update = content_lines.last_update();
901 let expiration_date = content_lines.expiration_date();
902 let hash = content_lines.hash();
903
904 let leap_second_lines = parse_leap_second_lines(&content_lines.leap_seconds)?;
905
906 let calculated_hash = calculate_hash(last_update, expiration_date, &leap_second_lines);
907
908 let last_update = parse_timestamp(last_update)?;
909 let expiration_date = parse_timestamp(expiration_date)?;
910 let hash_from_file = parse_hash(hash)?;
911
912 let leap_seconds = parse_leap_seconds(&leap_second_lines)?;
913
914 if calculated_hash != hash_from_file {
915 return Err(ParseFileError::InvalidHash {
916 calculated: calculated_hash,
917 found: hash_from_file,
918 });
919 }
920
921 Ok(LeapSecondsList {
922 last_update,
923 expiration_date,
924 leap_seconds,
925 })
926 }
927
928 #[must_use]
930 pub fn is_expired(&self) -> bool {
931 self.expiration_date() <= Timestamp::now()
932 }
933
934 #[must_use]
936 pub const fn last_update(&self) -> Timestamp {
937 self.last_update
938 }
939
940 #[must_use]
942 pub const fn expiration_date(&self) -> Timestamp {
943 self.expiration_date
944 }
945
946 #[must_use]
948 pub fn leap_seconds(&self) -> &[LeapSecond] {
949 &self.leap_seconds
950 }
951
952 #[must_use]
954 pub fn into_leap_seconds(self) -> Vec<LeapSecond> {
955 self.leap_seconds
956 }
957
958 #[must_use]
960 pub fn leap_seconds_after(&self, timestamp: Timestamp) -> &[LeapSecond] {
961 let start_index = self
963 .leap_seconds()
964 .iter()
965 .enumerate()
966 .find(|(_, leap_second)| leap_second.timestamp() > timestamp)
967 .map_or_else(|| self.leap_seconds().len(), |(index, _)| index);
968
969 &self.leap_seconds()[start_index..]
970 }
971
972 #[must_use]
974 pub fn planned_leap_seconds(&self) -> &[LeapSecond] {
975 self.leap_seconds_after(Timestamp::now())
976 }
977
978 #[must_use]
982 pub fn next_leap_second_after(&self, timestamp: Timestamp) -> Option<LeapSecond> {
983 self.leap_seconds_after(timestamp).first().copied()
985 }
986
987 #[must_use]
994 pub fn next_leap_second(&self) -> Option<LeapSecond> {
995 self.next_leap_second_after(Timestamp::now())
996 }
997}
998
999#[cfg(test)]
1000mod proptests;
1001
1002#[cfg(test)]
1003mod tests {
1004 mod crate_doc_file_sources {
1005 use {
1006 crate::{LeapSecond, LeapSecondsList, Timestamp},
1007 std::io::BufReader,
1008 };
1009
1010 #[test]
1011 fn iers() {
1012 test_source("https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list")
1013 }
1014
1015 #[test]
1016 fn tzdb_iana() {
1017 test_source("https://data.iana.org/time-zones/tzdb/leap-seconds.list")
1018 }
1019
1020 #[test]
1021 fn tzdb_github() {
1022 test_source("https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list")
1023 }
1024
1025 #[test]
1026 fn meinberg() {
1027 test_source("https://www.meinberg.de/download/ntp/leap-seconds.list")
1028 }
1029
1030 fn test_source(url: &str) {
1031 let file = reqwest::blocking::get(url).unwrap();
1032 let leap_seconds_list =
1033 LeapSecondsList::new(BufReader::new(file)).expect("parsing should be successful");
1034
1035 assert!(!leap_seconds_list.is_expired());
1037
1038 let min_expiration_date = Timestamp::from_u64(3896899200);
1040 assert!(leap_seconds_list.expiration_date() >= min_expiration_date);
1041
1042 let min_last_update = Timestamp::from_u64(3676924800);
1044 assert!(leap_seconds_list.last_update() >= min_last_update);
1045
1046 let first_leap_second = &leap_seconds_list.leap_seconds()[0];
1048 let expected_timestamp = Timestamp::from_u64(2272060800);
1049 let expected_tai_diff = 10;
1050 assert_eq!(first_leap_second.timestamp(), expected_timestamp);
1051 assert_eq!(first_leap_second.tai_diff(), expected_tai_diff);
1052
1053 let expected = Some(LeapSecond {
1055 timestamp: Timestamp::from_u64(2950473600),
1056 tai_diff: 28,
1057 });
1058 let actual = leap_seconds_list.next_leap_second_after(Timestamp::from_u64(2918937600));
1059 assert_eq!(actual, expected);
1060
1061 let last_leap_second = leap_seconds_list
1063 .leap_seconds()
1064 .last()
1065 .expect("list contains at least 1 element");
1066 assert!(leap_seconds_list
1067 .leap_seconds_after(last_leap_second.timestamp())
1068 .is_empty());
1069 }
1070 }
1071
1072 mod timestamp {
1073 use {
1074 crate::{Date, DateTime, Time, Timestamp},
1075 chrono::{offset::Utc, Datelike, Timelike},
1076 };
1077
1078 #[test]
1079 fn now() {
1080 let expected = {
1081 let now = Utc::now();
1082 let date = now.date_naive();
1083 let time = now.time();
1084
1085 DateTime {
1086 date: Date::new(date.year() as u64, date.month() as u8, date.day() as u8)
1087 .expect("chrono produces valid dates"),
1088 time: Time::new(time.hour() as u8, time.minute() as u8, time.second() as u8)
1089 .expect("chrono produces valid times"),
1090 }
1091 };
1092 let actual = Timestamp::now().date_time();
1093
1094 assert_eq!(actual, expected);
1095 }
1096
1097 #[test]
1098 fn from_and_to_date_time_0() {
1099 let timestamp = Timestamp::from_u64(0);
1100 let date_time = timestamp.date_time();
1101 let expected_date_time = DateTime {
1102 date: Date::new(1900, 1, 1).unwrap(),
1103 time: Time::new(0, 0, 0).unwrap(),
1104 };
1105 assert_eq!(date_time, expected_date_time);
1106
1107 let timestamp_again =
1108 Timestamp::from_date_time(date_time).expect("should always be valid");
1109 assert_eq!(timestamp_again, timestamp);
1110
1111 let date_time_again = timestamp_again.date_time();
1112 assert_eq!(date_time_again, date_time);
1113 }
1114
1115 #[test]
1116 fn from_and_to_date_time_1889385054048000() {
1117 let timestamp = Timestamp::from_u64(1889385054048000);
1118 let date_time = timestamp.date_time();
1119
1120 let timestamp_again =
1121 Timestamp::from_date_time(date_time).expect("should always be valid");
1122 assert_eq!(timestamp_again, timestamp);
1123
1124 let date_time_again = timestamp_again.date_time();
1125 assert_eq!(date_time_again, date_time);
1126 }
1127
1128 #[test]
1129 fn from_and_to_date_time_2004317826065173() {
1130 let timestamp = Timestamp::from_u64(2004317826065173);
1131 let date_time = timestamp.date_time();
1132
1133 let timestamp_again =
1134 Timestamp::from_date_time(date_time).expect("should always be valid");
1135 assert_eq!(timestamp_again, timestamp);
1136
1137 let date_time_again = timestamp_again.date_time();
1138 assert_eq!(date_time_again, date_time);
1139 }
1140
1141 #[test]
1142 fn from_pre_1900_date_time() {
1143 let date_time = DateTime {
1144 date: Date::new(1899, 12, 31).unwrap(),
1145 time: Time::new(23, 59, 59).unwrap(),
1146 };
1147
1148 let error = Timestamp::from_date_time(date_time);
1149
1150 assert!(error.is_err());
1151 }
1152
1153 #[test]
1154 fn from_and_as_u64() {
1155 let original = 123456780987654;
1156 let result = Timestamp::from_u64(original).as_u64();
1157
1158 assert_eq!(result, original);
1159 }
1160
1161 #[test]
1162 fn test_1900_01_01() {
1163 let expected = Timestamp::from_date_time(DateTime {
1164 date: Date::new(1900, 1, 1).unwrap(),
1165 time: Time::new(0, 0, 0).unwrap(),
1166 })
1167 .unwrap();
1168 let actual = Timestamp::from_u64(0);
1169
1170 assert_eq!(actual, expected);
1171 }
1172
1173 #[test]
1174 fn test_1901_01_07_19_45_33() {
1175 let year = 1 * 365 * 24 * 60 * 60;
1176 let day = 6 * 24 * 60 * 60;
1177 let hours = 19 * 60 * 60;
1178 let minutes = 45 * 60;
1179 let seconds = 33;
1180
1181 let expected = Timestamp::from_date_time(DateTime {
1182 date: Date::new(1901, 1, 7).unwrap(),
1183 time: Time::new(19, 45, 33).unwrap(),
1184 })
1185 .unwrap();
1186 let actual = Timestamp::from_u64(year + day + hours + minutes + seconds);
1187
1188 assert_eq!(actual, expected);
1189 }
1190
1191 #[test]
1192 fn test_1904_02_29_23_59_59() {
1193 let year = 4 * 365 * 24 * 60 * 60;
1194 let month = 31 * 24 * 60 * 60;
1195 let day = 28 * 24 * 60 * 60;
1196 let hours = 23 * 60 * 60;
1197 let minutes = 59 * 60;
1198 let seconds = 59;
1199
1200 let timestamp = Timestamp::from_u64(year + month + day + hours + minutes + seconds);
1201 let expected = Timestamp::from_date_time(DateTime {
1202 date: Date::new(1904, 2, 29).unwrap(),
1203 time: Time::new(23, 59, 59).unwrap(),
1204 })
1205 .unwrap();
1206
1207 assert_eq!(timestamp, expected);
1208
1209 let next_timestamp = Timestamp::from_u64(timestamp.as_u64() + 1);
1210 let next_expected = Timestamp::from_date_time(DateTime {
1211 date: Date::new(1904, 3, 1).unwrap(),
1212 time: Time::new(0, 0, 0).unwrap(),
1213 })
1214 .unwrap();
1215
1216 assert_eq!(next_timestamp, next_expected);
1217
1218 assert!(next_timestamp > timestamp);
1219 }
1220
1221 #[test]
1222 fn test_2023_06_28() {
1223 let expected = Timestamp::from_date_time(DateTime {
1224 date: Date::new(2023, 6, 28).unwrap(),
1225 time: Time::new(0, 0, 0).unwrap(),
1226 })
1227 .unwrap();
1228 let actual = Timestamp::from_u64(3896899200);
1229
1230 assert_eq!(actual, expected);
1231 }
1232
1233 #[test]
1234 fn test_1985_07_01() {
1235 let expected = Timestamp::from_date_time(DateTime {
1236 date: Date::new(1985, 7, 1).unwrap(),
1237 time: Time::new(0, 0, 0).unwrap(),
1238 })
1239 .unwrap();
1240 let actual = Timestamp::from_u64(2698012800);
1241
1242 assert_eq!(actual, expected);
1243 }
1244
1245 #[test]
1246 fn test_2017_01_01() {
1247 let expected = Timestamp::from_date_time(DateTime {
1248 date: Date::new(2017, 1, 1).unwrap(),
1249 time: Time::new(0, 0, 0).unwrap(),
1250 })
1251 .unwrap();
1252 let actual = Timestamp::from_u64(3692217600);
1253
1254 assert_eq!(actual, expected);
1255 }
1256 }
1257}