Kaynağa Gözat

util/time: implement DateTime::from_timestamp_str and corresponding unit test

Summary:
- Added `from_timestamp_str` method to convert timestamp strings in "YYYY-MM-DDTHH:mm" format into a `DateTime` instance or return an `Error::ParseFailed` for invalid inputs
- Implemented `is_valid_day` auxiliary function to validate day values, accounting for leap years
- Created unit test for `from_timestamp_str`, covering valid timestamps, boundary conditions, and various invalid formats
- Moved `MONTHS` to a top-level constant definition for shared access within the module and renamed it to `DAYS_IN_MONTHS`
- Changed the order that DateTime fields are declared to support sled natural ordering
- Added TODO to fix `timestamp_to_date` logic and add corresponding test case
kalm 1 yıl önce
ebeveyn
işleme
dd81943013
1 değiştirilmiş dosya ile 174 ekleme ve 26 silme
  1. 174 26
      src/util/time.rs

+ 174 - 26
src/util/time.rs

@@ -28,10 +28,25 @@ use crate::{Error, Result};
 const SECS_IN_DAY: u64 = 86400;
 const MIN_IN_HOUR: u64 = 60;
 const SECS_IN_HOUR: u64 = 3600;
+/// Represents the number of days in each month for both leap and non-leap years.
+const DAYS_IN_MONTHS: [[u64; 12]; 2] = [
+    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+    [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Leap years
+];
 
 /// Wrapper struct to represent system timestamps.
 #[derive(
-    Hash, Clone, Copy, Debug, SerialEncodable, SerialDecodable, PartialEq, PartialOrd, Ord, Eq,
+    Hash,
+    Clone,
+    Copy,
+    Debug,
+    SerialEncodable,
+    SerialDecodable,
+    PartialEq,
+    PartialOrd,
+    Ord,
+    Eq,
+    Default,
 )]
 pub struct Timestamp(u64);
 
@@ -82,8 +97,8 @@ impl From<u64> for Timestamp {
     }
 }
 
-impl std::fmt::Display for Timestamp {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+impl fmt::Display for Timestamp {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let date = timestamp_to_date(self.0, DateFormat::DateTime);
         write!(f, "{}", date)
     }
@@ -97,8 +112,8 @@ impl NanoTimestamp {
         Self(UNIX_EPOCH.elapsed().unwrap().as_nanos())
     }
 }
-impl std::fmt::Display for NanoTimestamp {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+impl fmt::Display for NanoTimestamp {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let date = timestamp_to_date(self.0.try_into().unwrap(), DateFormat::Nanos);
         write!(f, "{}", date)
     }
@@ -111,34 +126,30 @@ pub enum DateFormat {
     Nanos,
 }
 
-#[derive(Clone, Debug, Default)]
+/// Represents a UTC `DateTime` with individual fields for date and time components.
+#[derive(Clone, Debug, Default, Eq, PartialEq, SerialEncodable, SerialDecodable)]
 pub struct DateTime {
-    pub nanos: u32,
-    pub sec: u32,
-    pub min: u32,
-    pub hour: u32,
-    pub day: u32,
-    pub month: u32,
     pub year: u32,
+    pub month: u32,
+    pub day: u32,
+    pub hour: u32,
+    pub min: u32,
+    pub sec: u32,
+    pub nanos: u32,
 }
 
 impl DateTime {
     pub fn new() -> Self {
-        Self { nanos: 0, sec: 0, min: 0, hour: 0, day: 0, month: 0, year: 0 }
+        Self { year: 0, month: 0, day: 0, hour: 0, min: 0, sec: 0, nanos: 0 }
     }
 
     pub fn date(&self) -> Date {
-        Date { day: self.day, month: self.month, year: self.year }
+        Date { year: self.year, month: self.month, day: self.day }
     }
 
     pub fn from_timestamp(secs: u64, nsecs: u32) -> Self {
         let leap_year = |year| -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) };
 
-        static MONTHS: [[u64; 12]; 2] = [
-            [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
-            [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
-        ];
-
         let mut date_time = DateTime::new();
         let mut year = 1970;
 
@@ -162,8 +173,8 @@ impl DateTime {
         date_time.year = year;
 
         let mut month = 0;
-        while day_number >= MONTHS[if leap_year(year) { 1 } else { 0 }][month] {
-            day_number -= MONTHS[if leap_year(year) { 1 } else { 0 }][month];
+        while day_number >= DAYS_IN_MONTHS[if leap_year(year) { 1 } else { 0 }][month] {
+            day_number -= DAYS_IN_MONTHS[if leap_year(year) { 1 } else { 0 }][month];
             month += 1;
         }
         date_time.month = month as u32 + 1;
@@ -171,13 +182,80 @@ impl DateTime {
 
         date_time
     }
+
+    /// Provides a `DateTime` instance from a string in "YYYY-MM-DDTHH:mm:ss" format.
+    ///
+    /// This function parses and validates the timestamp string, returning a `DateTime` instance
+    /// with the parsed year, month, day, hour, minute, and second. Nanoseconds are not included
+    /// in the input string and default to zero. If the input string does not match the expected
+    /// format or contains invalid date or time values, it returns an [`Error::ParseFailed`] error.
+    pub fn from_timestamp_str(timestamp_str: &str) -> Result<Self> {
+        // Split the input string into date and time based on the 'T' separator
+        let parts: Vec<&str> = timestamp_str.split('T').collect();
+
+        // Check if the split parts have the correct length
+        if parts.len() != 2 {
+            return Err(Error::ParseFailed("Invalid timestamp format"));
+        }
+
+        // Parse the date into a vec
+        let date_components: Vec<u32> = parts[0]
+            .split('-')
+            .map(|s| s.parse::<u32>().map_err(|_| Error::ParseFailed("Invalid date component")))
+            .collect::<Result<Vec<u32>>>()?;
+
+        // Verify year, month, and day are provided
+        if date_components.len() != 3 {
+            return Err(Error::ParseFailed("Invalid date format"));
+        }
+
+        // Parse the time into a vec
+        let time_components: Vec<u32> = parts[1]
+            .split(':')
+            .map(|s| s.parse::<u32>().map_err(|_| Error::ParseFailed("Invalid time component")))
+            .collect::<Result<Vec<u32>>>()?;
+
+        // Verify that hour, minute, second are provided
+        if time_components.len() != 3 {
+            return Err(Error::ParseFailed("Invalid time format"));
+        }
+
+        // Destructure the date components into year, month, and day
+        let (year, month, day) = (date_components[0], date_components[1], date_components[2]);
+
+        // Validate month and day
+        if !(1..=12).contains(&month) || !Self::is_valid_day(year, month, day) {
+            return Err(Error::ParseFailed("Invalid month or day"));
+        }
+
+        // Destructure the time components into hour, minute, and second
+        let (hour, min, sec) = (time_components[0], time_components[1], time_components[2]);
+
+        // Validate hour, minute, and second values
+        if hour > 23 || min > 59 || sec > 59 {
+            return Err(Error::ParseFailed("Invalid hour, minute or second"));
+        }
+
+        // Return a new DateTime instance with parsed values and default nanoseconds set to 0
+        Ok(DateTime { year, month, day, hour, min, sec, nanos: 0 })
+    }
+
+    /// Auxiliary function that determines whether the specified day is within the valid range
+    /// for the given month and year, accounting for leap years. It returns `true` if the day
+    /// is valid.
+    fn is_valid_day(year: u32, month: u32, day: u32) -> bool {
+        let days_in_month = DAYS_IN_MONTHS
+            [(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) as usize]
+            [(month - 1) as usize];
+        day > 0 && day <= days_in_month as u32
+    }
 }
 
 impl fmt::Display for DateTime {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(
             f,
-            "{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
+            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
             self.year, self.month, self.day, self.hour, self.min, self.sec
         )
     }
@@ -192,13 +270,14 @@ pub struct Date {
 
 impl fmt::Display for Date {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{:04}-{:02}-{:02} UTC", self.year, self.month, self.day)
+        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
     }
 }
 
+// TODO: fix logic and add corresponding test case
 pub fn timestamp_to_date(timestamp: u64, format: DateFormat) -> String {
     if timestamp == 0 {
-        return "".to_string()
+        return "".to_string();
     }
 
     match format {
@@ -210,7 +289,7 @@ pub fn timestamp_to_date(timestamp: u64, format: DateFormat) -> String {
             let dt =
                 DateTime::from_timestamp(timestamp / A_BILLION, (timestamp % A_BILLION) as u32);
             format!(
-                "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{} UTC",
+                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{}",
                 dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, dt.nanos
             )
         }
@@ -219,7 +298,7 @@ pub fn timestamp_to_date(timestamp: u64, format: DateFormat) -> String {
 
 #[cfg(test)]
 mod tests {
-    use super::Timestamp;
+    use super::{DateTime, Timestamp};
 
     #[test]
     fn check_ts_add_overflow() {
@@ -231,4 +310,73 @@ mod tests {
         let cur = Timestamp::current_time().checked_add(10_000.into()).unwrap();
         assert!(cur.elapsed().is_err());
     }
+
+    #[test]
+    /// Tests the `from_timestamp_str` function to ensure it correctly converts timestamp strings into `DateTime` instances.
+    fn test_from_timestamp_str() {
+        // Verify validate dates
+        let valid_timestamps = vec![
+            (
+                "2024-01-01T12:00:00",
+                DateTime { year: 2024, month: 1, day: 1, hour: 12, min: 0, sec: 0, nanos: 0 },
+            ),
+            (
+                "2024-02-29T23:59:59",
+                DateTime { year: 2024, month: 2, day: 29, hour: 23, min: 59, sec: 59, nanos: 0 },
+            ), // Leap year
+            (
+                "2023-12-31T00:00:00",
+                DateTime { year: 2023, month: 12, day: 31, hour: 0, min: 0, sec: 0, nanos: 0 },
+            ),
+            (
+                "1970-01-01T00:00:00",
+                DateTime { year: 1970, month: 1, day: 1, hour: 0, min: 0, sec: 0, nanos: 0 },
+            ), // Unix epoch
+        ];
+
+        for (timestamp_str, expected) in valid_timestamps {
+            let result = DateTime::from_timestamp_str(timestamp_str)
+                .expect("Valid timestamp should not fail");
+            assert_eq!(result, expected);
+        }
+
+        // Verify boundary conditions
+        let boundary_timestamps = vec![
+            (
+                "2023-02-28T23:59:59",
+                DateTime { year: 2023, month: 2, day: 28, hour: 23, min: 59, sec: 59, nanos: 0 },
+            ),
+            (
+                "2023-03-01T00:00:00",
+                DateTime { year: 2023, month: 3, day: 1, hour: 0, min: 0, sec: 0, nanos: 0 },
+            ),
+            (
+                "2024-02-29T12:30:30",
+                DateTime { year: 2024, month: 2, day: 29, hour: 12, min: 30, sec: 30, nanos: 0 },
+            ), // Leap year
+        ];
+
+        for (timestamp_str, expected) in boundary_timestamps {
+            let result = DateTime::from_timestamp_str(timestamp_str)
+                .expect("Valid timestamp should not fail");
+            assert_eq!(result, expected);
+        }
+
+        // Verify invalid timestamps
+        let invalid_timestamps = vec![
+            "2023-02-30T12:00:00",    // Invalid day
+            "2023-04-31T12:00:00",    // Invalid day
+            "2023-13-01T12:00:00",    // Invalid month
+            "2023-01-01T12.00.00",    // Invalid format
+            "2023-01-01",             // Missing time part
+            "2023-01-01 12.00.00",    // Missing T separator
+            "2023/01/01T12:00",       // Incorrect date separator
+            "2023-01-01T-12:-60:-60", // Invalid time components
+        ];
+
+        for timestamp_str in invalid_timestamps {
+            let result = DateTime::from_timestamp_str(timestamp_str);
+            assert!(result.is_err(), "Expected error for invalid timestamp '{}'", timestamp_str);
+        }
+    }
 }