use std::{cmp::Ordering, fmt, str::FromStr};
use super::{Date, Time};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Timestamp {
pub date: Date,
pub time: Time,
}
impl Timestamp {
pub fn new(date: Date, time: Time) -> Self {
Timestamp { date, time }
}
}
impl FromStr for Timestamp {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let timestamp_part = strip_timezone_suffix(trimmed);
if let Some(t_pos) = timestamp_part.find('T') {
let date_str = ×tamp_part[..t_pos];
let time_str = ×tamp_part[t_pos + 1..];
let date = Date::from_str(date_str)?;
let time = Time::from_str(time_str)?;
return Ok(Timestamp::new(date, time));
}
let parts: Vec<&str> = timestamp_part.split_whitespace().collect();
if parts.len() == 2 {
let date = Date::from_str(parts[0])?;
let time = Time::from_str(parts[1])?;
return Ok(Timestamp::new(date, time));
} else if parts.len() == 1 {
if let Ok(date) = Date::from_str(parts[0]) {
let midnight = Time::new(0, 0, 0, 0).unwrap();
return Ok(Timestamp::new(date, midnight));
}
}
Err(format!(
"Invalid timestamp format: '{}'. Supported formats: \
ISO 8601 (2025-11-10T08:24:34), \
space-separated (2025-11-10 08:24:34), \
or date only (2025-11-10)",
s
))
}
}
fn strip_timezone_suffix(s: &str) -> &str {
if s.ends_with('Z') || s.ends_with('z') {
return &s[..s.len() - 1];
}
if let Some(pos) = s.rfind(['+', '-']) {
if pos > 10 {
let potential_tz = &s[pos..];
if is_timezone_offset(potential_tz) {
return &s[..pos];
}
}
}
s
}
fn is_timezone_offset(s: &str) -> bool {
if s.len() < 3 {
return false;
}
let sign = s.chars().next().unwrap();
if sign != '+' && sign != '-' {
return false;
}
let rest = &s[1..];
if rest.len() == 5 && rest.chars().nth(2) == Some(':') {
return rest[..2].chars().all(|c| c.is_ascii_digit())
&& rest[3..].chars().all(|c| c.is_ascii_digit());
}
if rest.len() == 4 {
return rest.chars().all(|c| c.is_ascii_digit());
}
if rest.len() == 2 {
return rest.chars().all(|c| c.is_ascii_digit());
}
false
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.date, self.time)
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timestamp {
fn cmp(&self, other: &Self) -> Ordering {
self.date.cmp(&other.date).then_with(|| self.time.cmp(&other.time))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_pads_fraction_to_minimum_three_digits() {
let ts = Timestamp::from_str("2024-01-01 12:00:00.5").unwrap();
assert_eq!(ts.to_string(), "2024-01-01 12:00:00.500");
}
#[test]
fn display_preserves_sub_millisecond_digits() {
let ts = Timestamp::from_str("2024-01-01 14:30:00.123456").unwrap();
assert_eq!(ts.to_string(), "2024-01-01 14:30:00.123456");
}
#[test]
fn display_parse_round_trip_preserves_value() {
for s in [
"2024-01-01 12:00:00",
"2024-01-01 12:00:00.5",
"2024-01-01 12:00:00.500",
"2024-01-01 14:30:00.123456",
"2024-01-01 14:30:00.123456789",
] {
let ts = Timestamp::from_str(s).unwrap();
let reparsed = Timestamp::from_str(&ts.to_string()).unwrap();
assert_eq!(reparsed, ts, "round-trip failed for '{}' (rendered '{}')", s, ts);
}
}
}