use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use super::error::{OtomlError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OTime(u32);
impl OTime {
pub fn from_epoch(seconds: u32) -> Self {
OTime(seconds)
}
pub fn epoch(&self) -> u32 {
self.0
}
pub fn from_components(
year: u32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Result<Self> {
if !(1970..=2106).contains(&year) {
return Err(OtomlError::InvalidTime(format!(
"year {} out of range (1970-2106)",
year
)));
}
if !(1..=12).contains(&month) {
return Err(OtomlError::InvalidTime(format!(
"month {} out of range (1-12)",
month
)));
}
if !(1..=31).contains(&day) {
return Err(OtomlError::InvalidTime(format!(
"day {} out of range (1-31)",
day
)));
}
if hour > 23 {
return Err(OtomlError::InvalidTime(format!(
"hour {} out of range (0-23)",
hour
)));
}
if minute > 59 {
return Err(OtomlError::InvalidTime(format!(
"minute {} out of range (0-59)",
minute
)));
}
if second > 59 {
return Err(OtomlError::InvalidTime(format!(
"second {} out of range (0-59)",
second
)));
}
let days_in_month = days_in_month(year, month);
if day > days_in_month {
return Err(OtomlError::InvalidTime(format!(
"day {} invalid for month {} (max {})",
day, month, days_in_month
)));
}
let epoch = date_to_epoch(year, month, day, hour, minute, second);
if epoch > u32::MAX as u64 {
return Err(OtomlError::InvalidTime(format!(
"date {}-{:02}-{:02} exceeds u32 range",
year, month, day
)));
}
Ok(OTime(epoch as u32))
}
pub fn components(&self) -> (u32, u32, u32, u32, u32, u32) {
epoch_to_date(self.0)
}
pub fn now() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
OTime((secs & 0xFFFFFFFF) as u32)
}
}
impl Default for OTime {
fn default() -> Self {
OTime(0)
}
}
impl fmt::Display for OTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (year, month, day, hour, minute, second) = self.components();
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
year, month, day, hour, minute, second
)
}
}
impl FromStr for OTime {
type Err = OtomlError;
fn from_str(s: &str) -> Result<Self> {
let s = s.trim();
if s.len() != 19 {
return Err(OtomlError::InvalidTime(format!(
"invalid length {}, expected 19 (YYYY-MM-DD HH:MM:SS)",
s.len()
)));
}
let bytes = s.as_bytes();
if bytes[4] != b'-'
|| bytes[7] != b'-'
|| bytes[10] != b' '
|| bytes[13] != b':'
|| bytes[16] != b':'
{
return Err(OtomlError::InvalidTime(format!(
"invalid format, expected YYYY-MM-DD HH:MM:SS, got '{}'",
s
)));
}
let year: u32 = s[0..4]
.parse()
.map_err(|_| OtomlError::InvalidTime("invalid year".to_string()))?;
let month: u32 = s[5..7]
.parse()
.map_err(|_| OtomlError::InvalidTime("invalid month".to_string()))?;
let day: u32 = s[8..10]
.parse()
.map_err(|_| OtomlError::InvalidTime("invalid day".to_string()))?;
let hour: u32 = s[11..13]
.parse()
.map_err(|_| OtomlError::InvalidTime("invalid hour".to_string()))?;
let minute: u32 = s[14..16]
.parse()
.map_err(|_| OtomlError::InvalidTime("invalid minute".to_string()))?;
let second: u32 = s[17..19]
.parse()
.map_err(|_| OtomlError::InvalidTime("invalid second".to_string()))?;
OTime::from_components(year, month, day, hour, minute, second)
}
}
impl Serialize for OTime {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for OTime {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
OTime::from_str(&s).map_err(serde::de::Error::custom)
}
}
fn is_leap_year(year: u32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 => 31,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
_ => 0,
}
}
fn date_to_epoch(year: u32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u64 {
let mut days: u64 = 0;
for y in 1970..year {
days += if is_leap_year(y) { 366 } else { 365 };
}
for m in 1..month {
days += days_in_month(year, m) as u64;
}
days += (day - 1) as u64;
days * 86400 + (hour as u64) * 3600 + (minute as u64) * 60 + (second as u64)
}
fn epoch_to_date(epoch: u32) -> (u32, u32, u32, u32, u32, u32) {
let mut remaining = epoch as u64;
let second = (remaining % 60) as u32;
remaining /= 60;
let minute = (remaining % 60) as u32;
remaining /= 60;
let hour = (remaining % 24) as u32;
let mut days = remaining / 24;
let mut year = 1970u32;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let mut month = 1u32;
loop {
let days_in_m = days_in_month(year, month) as u64;
if days < days_in_m {
break;
}
days -= days_in_m;
month += 1;
}
let day = (days + 1) as u32;
(year, month, day, hour, minute, second)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_epoch_zero() {
let time = OTime::from_epoch(0);
assert_eq!(time.to_string(), "1970-01-01 00:00:00");
assert_eq!(time.epoch(), 0);
}
#[test]
fn test_parse_and_format() {
let time: OTime = "2025-01-03 14:22:59".parse().unwrap();
assert_eq!(time.to_string(), "2025-01-03 14:22:59");
}
#[test]
fn test_roundtrip() {
let original = "2038-01-19 03:14:07";
let time: OTime = original.parse().unwrap();
assert_eq!(time.to_string(), original);
}
#[test]
fn test_from_components() {
let time = OTime::from_components(2025, 6, 15, 12, 30, 45).unwrap();
let (year, month, day, hour, minute, second) = time.components();
assert_eq!(year, 2025);
assert_eq!(month, 6);
assert_eq!(day, 15);
assert_eq!(hour, 12);
assert_eq!(minute, 30);
assert_eq!(second, 45);
}
#[test]
fn test_leap_year() {
let time = OTime::from_components(2024, 2, 29, 0, 0, 0).unwrap();
assert_eq!(time.to_string(), "2024-02-29 00:00:00");
let result = OTime::from_components(2025, 2, 29, 0, 0, 0);
assert!(result.is_err());
}
#[test]
fn test_invalid_format() {
assert!("2025-01-03".parse::<OTime>().is_err());
assert!("2025/01/03 14:22:59".parse::<OTime>().is_err());
assert!("2025-01-03T14:22:59".parse::<OTime>().is_err());
assert!("2025-13-03 14:22:59".parse::<OTime>().is_err()); assert!("2025-01-32 14:22:59".parse::<OTime>().is_err()); assert!("2025-01-03 25:22:59".parse::<OTime>().is_err()); }
#[test]
fn test_comparison() {
let t1: OTime = "2025-01-01 00:00:00".parse().unwrap();
let t2: OTime = "2025-01-02 00:00:00".parse().unwrap();
assert!(t1 < t2);
assert!(t2 > t1);
}
#[test]
fn test_serde_roundtrip() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Doc {
created_at: OTime,
}
let doc = Doc {
created_at: "2025-01-03 14:22:59".parse().unwrap(),
};
let otoml = crate::dump_otoml(&doc).unwrap();
assert!(otoml.contains("created_at = \"2025-01-03 14:22:59\""));
let parsed: Doc = crate::load_otoml(&otoml).unwrap();
assert_eq!(doc, parsed);
}
#[test]
fn test_y2038() {
let time: OTime = "2038-01-19 03:14:07".parse().unwrap();
assert_eq!(time.epoch(), 2147483647);
let time: OTime = "2038-01-19 03:14:08".parse().unwrap();
assert_eq!(time.epoch(), 2147483648);
}
#[test]
fn test_max_date() {
let time = OTime::from_components(2106, 2, 7, 6, 28, 15).unwrap();
assert!(time.epoch() <= u32::MAX);
}
#[test]
fn test_binary_roundtrip() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Doc {
created_at: OTime,
updated_at: Option<OTime>,
}
let doc = Doc {
created_at: "2025-01-03 14:22:59".parse().unwrap(),
updated_at: Some("2025-06-15 10:00:00".parse().unwrap()),
};
let bytes = crate::dump_obin(&doc).unwrap();
let parsed: Doc = crate::load_obin(&bytes).unwrap();
assert_eq!(doc, parsed);
}
#[test]
fn test_default() {
let time = OTime::default();
assert_eq!(time.epoch(), 0);
assert_eq!(time.to_string(), "1970-01-01 00:00:00");
}
#[test]
fn test_hash() {
use std::collections::HashSet;
let t1: OTime = "2025-01-01 00:00:00".parse().unwrap();
let t2: OTime = "2025-01-01 00:00:00".parse().unwrap();
let t3: OTime = "2025-01-02 00:00:00".parse().unwrap();
let mut set = HashSet::new();
set.insert(t1);
set.insert(t2); set.insert(t3);
assert_eq!(set.len(), 2);
}
#[test]
fn test_clone_and_copy() {
let t1: OTime = "2025-01-01 00:00:00".parse().unwrap();
let t2 = t1; let t3 = t1.clone();
assert_eq!(t1, t2);
assert_eq!(t1, t3);
}
#[test]
fn test_invalid_component_ranges() {
assert!(OTime::from_components(1969, 12, 31, 23, 59, 59).is_err());
assert!(OTime::from_components(2107, 1, 1, 0, 0, 0).is_err());
assert!(OTime::from_components(2025, 0, 1, 0, 0, 0).is_err());
assert!(OTime::from_components(2025, 13, 1, 0, 0, 0).is_err());
assert!(OTime::from_components(2025, 1, 0, 0, 0, 0).is_err());
assert!(OTime::from_components(2025, 1, 32, 0, 0, 0).is_err());
assert!(OTime::from_components(2025, 1, 1, 24, 0, 0).is_err());
assert!(OTime::from_components(2025, 1, 1, 0, 60, 0).is_err());
assert!(OTime::from_components(2025, 1, 1, 0, 0, 60).is_err());
}
#[test]
fn test_edge_dates() {
let t = OTime::from_components(1970, 1, 1, 0, 0, 0).unwrap();
assert_eq!(t.epoch(), 0);
let months_with_31 = [1, 3, 5, 7, 8, 10, 12];
for m in months_with_31 {
assert!(OTime::from_components(2025, m, 31, 23, 59, 59).is_ok());
}
let months_with_30 = [4, 6, 9, 11];
for m in months_with_30 {
assert!(OTime::from_components(2025, m, 30, 23, 59, 59).is_ok());
assert!(OTime::from_components(2025, m, 31, 0, 0, 0).is_err());
}
}
#[test]
fn test_century_leap_years() {
assert!(OTime::from_components(2000, 2, 29, 0, 0, 0).is_ok());
assert!(OTime::from_components(2100, 2, 29, 0, 0, 0).is_err());
assert!(OTime::from_components(2100, 2, 28, 0, 0, 0).is_ok());
}
}