time.rs
1 /* This file is part of DarkFi (https://dark.fi) 2 * 3 * Copyright (C) 2020-2025 Dyne.org foundation 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation, either version 3 of the 8 * License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Affero General Public License for more details. 14 * 15 * You should have received a copy of the GNU Affero General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19 use std::{ 20 fmt, 21 time::{Duration, UNIX_EPOCH}, 22 }; 23 24 #[cfg(feature = "async-serial")] 25 use darkfi_serial::async_trait; 26 27 use darkfi_serial::{SerialDecodable, SerialEncodable}; 28 29 use crate::{Error, Result}; 30 31 const SECS_IN_DAY: u64 = 86400; 32 const MIN_IN_HOUR: u64 = 60; 33 const SECS_IN_HOUR: u64 = 3600; 34 /// Represents the number of days in each month for both leap and non-leap years. 35 const DAYS_IN_MONTHS: [[u64; 12]; 2] = [ 36 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], 37 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Leap years 38 ]; 39 40 /// Wrapper struct to represent system timestamps. 41 #[derive( 42 Hash, 43 Clone, 44 Copy, 45 Debug, 46 SerialEncodable, 47 SerialDecodable, 48 PartialEq, 49 PartialOrd, 50 Ord, 51 Eq, 52 Default, 53 )] 54 pub struct Timestamp(u64); 55 56 impl Timestamp { 57 /// Returns the inner `u64` of `Timestamp` 58 pub fn inner(&self) -> u64 { 59 self.0 60 } 61 62 /// Generate a `Timestamp` of the current time. 63 pub fn current_time() -> Self { 64 Self(UNIX_EPOCH.elapsed().unwrap().as_secs()) 65 } 66 67 /// Calculates the elapsed time of a `Timestamp` up to the time of calling the function. 68 pub fn elapsed(&self) -> Result<Self> { 69 Self::current_time().checked_sub(*self) 70 } 71 72 /// Add `self` to a given timestamp 73 /// Errors on integer overflow. 74 pub fn checked_add(&self, ts: Self) -> Result<Self> { 75 if let Some(result) = self.inner().checked_add(ts.inner()) { 76 Ok(Self(result)) 77 } else { 78 Err(Error::AdditionOverflow) 79 } 80 } 81 82 /// Subtract `self` with a given timestamp 83 /// Errors on integer underflow. 84 pub fn checked_sub(&self, ts: Self) -> Result<Self> { 85 if let Some(result) = self.inner().checked_sub(ts.inner()) { 86 Ok(Self(result)) 87 } else { 88 Err(Error::SubtractionUnderflow) 89 } 90 } 91 92 pub const fn from_u64(x: u64) -> Self { 93 Self(x) 94 } 95 } 96 97 impl From<u64> for Timestamp { 98 fn from(x: u64) -> Self { 99 Self(x) 100 } 101 } 102 103 impl fmt::Display for Timestamp { 104 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 let date = timestamp_to_date(self.0, DateFormat::DateTime); 106 write!(f, "{date}") 107 } 108 } 109 110 #[derive(Clone, Copy, Debug, SerialEncodable, SerialDecodable, PartialEq, PartialOrd, Eq)] 111 pub struct NanoTimestamp(pub u128); 112 113 impl NanoTimestamp { 114 pub fn inner(&self) -> u128 { 115 self.0 116 } 117 118 pub const fn from_secs(secs: u128) -> Self { 119 Self(secs * 1_000_000_000) 120 } 121 122 pub fn current_time() -> Self { 123 Self(UNIX_EPOCH.elapsed().unwrap().as_nanos()) 124 } 125 126 pub fn elapsed(&self) -> Result<Self> { 127 Self::current_time().checked_sub(*self) 128 } 129 130 pub fn checked_sub(&self, ts: Self) -> Result<Self> { 131 if let Some(result) = self.inner().checked_sub(ts.inner()) { 132 Ok(Self(result)) 133 } else { 134 Err(Error::SubtractionUnderflow) 135 } 136 } 137 } 138 impl fmt::Display for NanoTimestamp { 139 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 140 let date = timestamp_to_date(self.0.try_into().unwrap(), DateFormat::Nanos); 141 write!(f, "{date}") 142 } 143 } 144 145 pub enum DateFormat { 146 Default, 147 Date, 148 DateTime, 149 Nanos, 150 } 151 152 /// Represents a UTC `DateTime` with individual fields for date and time components. 153 #[derive(Clone, Debug, Default, Eq, PartialEq, SerialEncodable, SerialDecodable)] 154 pub struct DateTime { 155 pub year: u32, 156 pub month: u32, 157 pub day: u32, 158 pub hour: u32, 159 pub min: u32, 160 pub sec: u32, 161 pub nanos: u32, 162 } 163 164 impl DateTime { 165 pub fn new() -> Self { 166 Self { year: 0, month: 0, day: 0, hour: 0, min: 0, sec: 0, nanos: 0 } 167 } 168 169 pub fn date(&self) -> Date { 170 Date { year: self.year, month: self.month, day: self.day } 171 } 172 173 pub fn from_timestamp(secs: u64, nsecs: u32) -> Self { 174 let leap_year = |year| -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) }; 175 176 let mut date_time = DateTime::new(); 177 let mut year = 1970; 178 179 let time = secs % SECS_IN_DAY; 180 let mut day_number = secs / SECS_IN_DAY; 181 182 date_time.nanos = nsecs; 183 date_time.sec = (time % MIN_IN_HOUR) as u32; 184 date_time.min = ((time % SECS_IN_HOUR) / MIN_IN_HOUR) as u32; 185 date_time.hour = (time / SECS_IN_HOUR) as u32; 186 187 loop { 188 let year_size = if leap_year(year) { 366 } else { 365 }; 189 if day_number >= year_size { 190 day_number -= year_size; 191 year += 1; 192 } else { 193 break 194 } 195 } 196 date_time.year = year; 197 198 let mut month = 0; 199 while day_number >= DAYS_IN_MONTHS[if leap_year(year) { 1 } else { 0 }][month] { 200 day_number -= DAYS_IN_MONTHS[if leap_year(year) { 1 } else { 0 }][month]; 201 month += 1; 202 } 203 date_time.month = month as u32 + 1; 204 date_time.day = day_number as u32 + 1; 205 206 date_time 207 } 208 209 /// Provides a `DateTime` instance from a string in "YYYY-MM-DDTHH:mm:ss" format. 210 /// 211 /// This function parses and validates the timestamp string, returning a `DateTime` instance 212 /// with the parsed year, month, day, hour, minute, and second. Nanoseconds are not included 213 /// in the input string and default to zero. If the input string does not match the expected 214 /// format or contains invalid date or time values, it returns an [`Error::ParseFailed`] error. 215 pub fn from_timestamp_str(timestamp_str: &str) -> Result<Self> { 216 // Split the input string into date and time based on the 'T' separator 217 let parts: Vec<&str> = timestamp_str.split('T').collect(); 218 219 // Check if the split parts have the correct length 220 if parts.len() != 2 { 221 return Err(Error::ParseFailed("Invalid timestamp format")); 222 } 223 224 // Parse the date into a vec 225 let date_components: Vec<u32> = parts[0] 226 .split('-') 227 .map(|s| s.parse::<u32>().map_err(|_| Error::ParseFailed("Invalid date component"))) 228 .collect::<Result<Vec<u32>>>()?; 229 230 // Verify year, month, and day are provided 231 if date_components.len() != 3 { 232 return Err(Error::ParseFailed("Invalid date format")); 233 } 234 235 // Parse the time into a vec 236 let time_components: Vec<u32> = parts[1] 237 .split(':') 238 .map(|s| s.parse::<u32>().map_err(|_| Error::ParseFailed("Invalid time component"))) 239 .collect::<Result<Vec<u32>>>()?; 240 241 // Verify that hour, minute, second are provided 242 if time_components.len() != 3 { 243 return Err(Error::ParseFailed("Invalid time format")); 244 } 245 246 // Destructure the date components into year, month, and day 247 let (year, month, day) = (date_components[0], date_components[1], date_components[2]); 248 249 // Validate month and day 250 if !(1..=12).contains(&month) || !Self::is_valid_day(year, month, day) { 251 return Err(Error::ParseFailed("Invalid month or day")); 252 } 253 254 // Destructure the time components into hour, minute, and second 255 let (hour, min, sec) = (time_components[0], time_components[1], time_components[2]); 256 257 // Validate hour, minute, and second values 258 if hour > 23 || min > 59 || sec > 59 { 259 return Err(Error::ParseFailed("Invalid hour, minute or second")); 260 } 261 262 // Return a new DateTime instance with parsed values and default nanoseconds set to 0 263 Ok(DateTime { year, month, day, hour, min, sec, nanos: 0 }) 264 } 265 266 /// Auxiliary function that determines whether the specified day is within the valid range 267 /// for the given month and year, accounting for leap years. It returns `true` if the day 268 /// is valid. 269 fn is_valid_day(year: u32, month: u32, day: u32) -> bool { 270 let days_in_month = DAYS_IN_MONTHS 271 [(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) as usize] 272 [(month - 1) as usize]; 273 day > 0 && day <= days_in_month as u32 274 } 275 } 276 277 impl fmt::Display for DateTime { 278 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 279 write!( 280 f, 281 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", 282 self.year, self.month, self.day, self.hour, self.min, self.sec 283 ) 284 } 285 } 286 287 #[derive(Clone, Debug, Default)] 288 pub struct Date { 289 pub day: u32, 290 pub month: u32, 291 pub year: u32, 292 } 293 294 impl fmt::Display for Date { 295 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 296 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day) 297 } 298 } 299 300 // TODO: fix logic and add corresponding test case 301 pub fn timestamp_to_date(timestamp: u64, format: DateFormat) -> String { 302 if timestamp == 0 { 303 return "".to_string(); 304 } 305 306 match format { 307 DateFormat::Default => "".to_string(), 308 DateFormat::Date => DateTime::from_timestamp(timestamp, 0).date().to_string(), 309 DateFormat::DateTime => DateTime::from_timestamp(timestamp, 0).to_string(), 310 DateFormat::Nanos => { 311 const A_BILLION: u64 = 1_000_000_000; 312 let dt = 313 DateTime::from_timestamp(timestamp / A_BILLION, (timestamp % A_BILLION) as u32); 314 format!( 315 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{}", 316 dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, dt.nanos 317 ) 318 } 319 } 320 } 321 322 /// Formats a `Duration` into a user-friendly format using days, hours, minutes, and seconds, 323 /// and returns the formatted string. 324 /// 325 /// Durations less than one minute include fractional seconds with nanosecond precision (up to 9 decimal places), 326 /// while durations of one minute or longer display as whole seconds, rounded to the nearest second. 327 /// 328 /// The output format includes the following components: 329 /// - `{days}d` for days 330 /// - `{hours}h` for hours 331 /// - `{minutes}m` for minutes 332 /// - `{seconds}s` for seconds 333 /// 334 /// When all components are non-zero, the format appears as: 335 /// ```plaintext 336 /// {days}d {hours}h {minutes}m {seconds}s 337 /// ``` 338 pub fn fmt_duration(duration: Duration) -> String { 339 let total_secs = duration.as_secs_f64(); 340 341 // Calculate each time component 342 let days = (total_secs / 86400.0).floor() as u64; 343 let hours = ((total_secs % 86400.0) / 3600.0).floor() as u64; 344 let minutes = ((total_secs % 3600.0) / 60.0).floor() as u64; 345 346 // Calculate fractional seconds (rounding to nanosecond precision) 347 let seconds = (total_secs % 60.0 * 1_000_000_000.0).round() / 1_000_000_000.0; 348 349 let mut parts = Vec::new(); 350 351 // Include non-zero components for dys, hours and minutes 352 if days > 0 { 353 parts.push(format!("{days}d")); 354 } 355 if hours > 0 { 356 parts.push(format!("{hours}h")); 357 } 358 if minutes > 0 { 359 parts.push(format!("{minutes}m")); 360 } 361 362 // Include seconds if they are non-zero or if all other components are zero (i.e., 0s) 363 if seconds > 0.0 || (days == 0 && hours == 0 && minutes == 0) { 364 // For durations shorter than 1 minute, include fractional seconds up to 9 decimal places 365 if days == 0 && hours == 0 && minutes == 0 && seconds.fract() != 0.0 { 366 parts.push(format!("{seconds:.9}s")); 367 } else { 368 // Otherwise, include rounded whole seconds 369 parts.push(format!("{}s", seconds.round() as u64)); 370 } 371 } 372 373 parts.join(" ") 374 } 375 376 #[cfg(test)] 377 mod tests { 378 use super::{fmt_duration, DateTime, Timestamp}; 379 use std::time::Duration; 380 381 #[test] 382 fn check_ts_add_overflow() { 383 assert!(Timestamp::current_time().checked_add(u64::MAX.into()).is_err()); 384 } 385 386 #[test] 387 fn check_ts_sub_underflow() { 388 let cur = Timestamp::current_time().checked_add(10_000.into()).unwrap(); 389 assert!(cur.elapsed().is_err()); 390 } 391 392 #[test] 393 /// Tests the `from_timestamp_str` function to ensure it correctly converts timestamp strings into `DateTime` instances. 394 fn test_from_timestamp_str() { 395 // Verify validate dates 396 let valid_timestamps = vec![ 397 ( 398 "2024-01-01T12:00:00", 399 DateTime { year: 2024, month: 1, day: 1, hour: 12, min: 0, sec: 0, nanos: 0 }, 400 ), 401 ( 402 "2024-02-29T23:59:59", 403 DateTime { year: 2024, month: 2, day: 29, hour: 23, min: 59, sec: 59, nanos: 0 }, 404 ), // Leap year 405 ( 406 "2023-12-31T00:00:00", 407 DateTime { year: 2023, month: 12, day: 31, hour: 0, min: 0, sec: 0, nanos: 0 }, 408 ), 409 ( 410 "1970-01-01T00:00:00", 411 DateTime { year: 1970, month: 1, day: 1, hour: 0, min: 0, sec: 0, nanos: 0 }, 412 ), // Unix epoch 413 ]; 414 415 for (timestamp_str, expected) in valid_timestamps { 416 let result = DateTime::from_timestamp_str(timestamp_str) 417 .expect("Valid timestamp should not fail"); 418 assert_eq!(result, expected); 419 } 420 421 // Verify boundary conditions 422 let boundary_timestamps = vec![ 423 ( 424 "2023-02-28T23:59:59", 425 DateTime { year: 2023, month: 2, day: 28, hour: 23, min: 59, sec: 59, nanos: 0 }, 426 ), 427 ( 428 "2023-03-01T00:00:00", 429 DateTime { year: 2023, month: 3, day: 1, hour: 0, min: 0, sec: 0, nanos: 0 }, 430 ), 431 ( 432 "2024-02-29T12:30:30", 433 DateTime { year: 2024, month: 2, day: 29, hour: 12, min: 30, sec: 30, nanos: 0 }, 434 ), // Leap year 435 ]; 436 437 for (timestamp_str, expected) in boundary_timestamps { 438 let result = DateTime::from_timestamp_str(timestamp_str) 439 .expect("Valid timestamp should not fail"); 440 assert_eq!(result, expected); 441 } 442 443 // Verify invalid timestamps 444 let invalid_timestamps = vec![ 445 "2023-02-30T12:00:00", // Invalid day 446 "2023-04-31T12:00:00", // Invalid day 447 "2023-13-01T12:00:00", // Invalid month 448 "2023-01-01T12.00.00", // Invalid format 449 "2023-01-01", // Missing time part 450 "2023-01-01 12.00.00", // Missing T separator 451 "2023/01/01T12:00", // Incorrect date separator 452 "2023-01-01T-12:-60:-60", // Invalid time components 453 ]; 454 455 for timestamp_str in invalid_timestamps { 456 let result = DateTime::from_timestamp_str(timestamp_str); 457 assert!(result.is_err(), "Expected error for invalid timestamp '{timestamp_str}'"); 458 } 459 } 460 #[test] 461 /// Tests the `fmt_duration` function to ensure it correctly formats durations. 462 pub fn test_fmt_duration() { 463 // Zero duration (edge case) 464 let duration = Duration::new(0, 0); 465 assert_eq!(fmt_duration(duration), "0s"); 466 467 // Small durations with fractional seconds 468 let duration = Duration::new(0, 987654321); 469 assert_eq!(fmt_duration(duration), "0.987654321s"); 470 471 // Exactly 1 second 472 let duration = Duration::new(1, 0); 473 assert_eq!(fmt_duration(duration), "1s"); 474 475 // Exactly 59.987654321 seconds (just under a minute) 476 let duration = Duration::new(59, 987654321); 477 assert_eq!(fmt_duration(duration), "59.987654321s"); 478 479 // Exactly 1 minute 480 let duration = Duration::new(60, 0); 481 assert_eq!(fmt_duration(duration), "1m"); 482 483 // 1 minute and 1 second 484 let duration = Duration::new(61, 0); 485 assert_eq!(fmt_duration(duration), "1m 1s"); 486 487 // 1 hour 488 let duration = Duration::new(3600, 0); 489 assert_eq!(fmt_duration(duration), "1h"); 490 491 // 1 hour, 15 minutes, and 37 seconds 492 let duration = Duration::new(4537, 0); 493 assert_eq!(fmt_duration(duration), "1h 15m 37s"); 494 495 // Large duration with rounded seconds 496 let duration = Duration::new((12 * 86400) + (11 * 3600) + (59 * 60) + 59, 0); 497 assert_eq!(fmt_duration(duration), "12d 11h 59m 59s"); 498 } 499 }