use std::fmt;
use crate::date::date::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Iso8601 {
pub date: Date,
pub offset_str: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct IsoDuration {
pub years: u32,
pub months: u32,
pub days: u32,
pub hours: u32,
pub minutes: u32,
pub seconds: u32,
}
impl Iso8601 {
pub fn parse(s: &str) -> Result<Self, String> {
if s.is_empty() { return Err("String is empty".into()); }
let parts: Vec<&str> = s.split('T').collect();
if parts.len() != 2 {
return Err("Missing 'T' separator or invalid format".into());
}
let date_part = parts[0];
let time_part = parts[1].trim_end_matches('Z');
let (year, month, day) = Self::parse_date_part(date_part)?;
let (hour, minute, second) = Self::parse_time_part(time_part)?;
if !Self::is_valid_calendar(year, month, day, hour, minute, second) {
return Err("Semantically invalid date".into());
}
let date = Date {
year, month, day, hour, minute, second
};
Ok(Iso8601 {
date,
offset_str: None,
})
}
fn parse_date_part(s: &str) -> Result<(i32, u8, u8), String> {
let parse_num = |str_slice: &str| -> Result<u32, String> {
str_slice.parse::<u32>().map_err(|_| format!("Invalid number: {}", str_slice))
};
if s.contains('-') {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 3 { return Err("Invalid extended date format".into()); }
Ok((
parse_num(parts[0])? as i32,
parse_num(parts[1])? as u8,
parse_num(parts[2])? as u8
))
} else {
if s.len() != 8 { return Err("Invalid basic date length".into()); }
Ok((
parse_num(&s[0..4])? as i32,
parse_num(&s[4..6])? as u8,
parse_num(&s[6..8])? as u8
))
}
}
fn parse_time_part(s: &str) -> Result<(u8, u8, u8), String> {
let parse_num = |str_slice: &str| -> Result<u8, String> {
str_slice.parse::<u8>().map_err(|_| format!("Invalid number: {}", str_slice))
};
if s.contains(':') {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() < 2 { return Err("Invalid extended time format".into()); } let h = parse_num(parts[0])?;
let m = parse_num(parts[1])?;
let s = if parts.len() > 2 { parse_num(parts[2])? } else { 0 };
Ok((h, m, s))
} else {
match s.len() {
6 => Ok((parse_num(&s[0..2])?, parse_num(&s[2..4])?, parse_num(&s[4..6])?)),
4 => Ok((parse_num(&s[0..2])?, parse_num(&s[2..4])?, 0)),
_ => Err("Invalid basic time length".into())
}
}
}
pub fn to_iso8601(&self) -> String {
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
self.date.year, self.date.month, self.date.day, self.date.hour, self.date.minute, self.date.second)
}
pub fn to_iso8601_basic(&self) -> String {
format!("{:04}{:02}{:02}T{:02}{:02}{:02}",
self.date.year, self.date.month, self.date.day, self.date.hour, self.date.minute, self.date.second)
}
fn is_valid_calendar(y: i32, m: u8, d: u8, h: u8, min: u8, s: u8) -> bool {
if m < 1 || m > 12 || h > 23 || min > 59 || s > 60 { return false; }
let days_in_month = match m {
4 | 6 | 9 | 11 => 30,
2 => if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) { 29 } else { 28 },
_ => 31,
};
d >= 1 && d <= days_in_month
}
}
impl IsoDuration {
pub fn parse(s: &str) -> Result<Self, String> {
if !s.starts_with('P') {
return Err("Duration string must start with 'P'".into());
}
let mut dur = IsoDuration::default();
let mut num_buf = String::new();
let mut is_time_part = false;
for c in s.chars().skip(1) {
match c {
'0'..='9' => num_buf.push(c),
'T' => {
is_time_part = true;
if !num_buf.is_empty() { return Err("Unexpected number before 'T'".into()); }
},
'Y' => {
if is_time_part { return Err("Years not allowed in time part".into()); }
dur.years = num_buf.parse().map_err(|_| "Invalid year")?;
num_buf.clear();
},
'M' => {
let val = num_buf.parse().map_err(|_| "Invalid number for M")?;
if is_time_part { dur.minutes = val; } else { dur.months = val; }
num_buf.clear();
},
'D' => {
if is_time_part { return Err("Days not allowed in time part".into()); }
dur.days = num_buf.parse().map_err(|_| "Invalid day")?;
num_buf.clear();
},
'H' => {
if !is_time_part { return Err("Hours must be after 'T'".into()); }
dur.hours = num_buf.parse().map_err(|_| "Invalid hour")?;
num_buf.clear();
},
'S' => {
if !is_time_part { return Err("Seconds must be after 'T'".into()); }
dur.seconds = num_buf.parse().map_err(|_| "Invalid second")?;
num_buf.clear();
},
_ => return Err(format!("Invalid character in duration: {}", c)),
}
}
Ok(dur)
}
pub fn to_string(&self) -> String {
let mut s = String::from("P");
if self.years > 0 { s.push_str(&format!("{}Y", self.years)); }
if self.months > 0 { s.push_str(&format!("{}M", self.months)); }
if self.days > 0 { s.push_str(&format!("{}D", self.days)); }
if self.hours > 0 || self.minutes > 0 || self.seconds > 0 {
s.push('T');
if self.hours > 0 { s.push_str(&format!("{}H", self.hours)); }
if self.minutes > 0 { s.push_str(&format!("{}M", self.minutes)); }
if self.seconds > 0 { s.push_str(&format!("{}S", self.seconds)); }
}
if s == "P" { return "P0D".to_string(); }
s
}
}
impl fmt::Display for Iso8601 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_iso8601())
}
}
impl fmt::Display for IsoDuration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_extended_iso8601() {
let iso = Iso8601::parse("2023-11-23T14:30:05").unwrap();
assert_eq!(iso.date.year, 2023);
assert_eq!(iso.date.month, 11);
assert_eq!(iso.date.second, 5);
}
#[test]
fn test_parse_basic_iso8601() {
let iso = Iso8601::parse("20231123T143005").unwrap();
assert_eq!(iso.date.year, 2023);
assert_eq!(iso.date.month, 11);
assert_eq!(iso.date.day, 23);
assert_eq!(iso.date.minute, 30);
}
#[test]
fn test_iso_output_formats() {
let iso = Iso8601::parse("2023-11-23T14:30:00").unwrap();
assert_eq!(iso.to_iso8601(), "2023-11-23T14:30:00");
assert_eq!(iso.to_iso8601_basic(), "20231123T143000");
}
#[test]
fn test_duration_parsing_full() {
let raw = "P3Y6M4DT12H30M5S";
let dur = IsoDuration::parse(raw).expect("Valid duration");
assert_eq!(dur.years, 3);
assert_eq!(dur.months, 6);
assert_eq!(dur.days, 4);
assert_eq!(dur.hours, 12);
assert_eq!(dur.minutes, 30);
assert_eq!(dur.seconds, 5);
}
#[test]
fn test_duration_parsing_partial() {
let raw = "P1YT5S";
let dur = IsoDuration::parse(raw).unwrap();
assert_eq!(dur.years, 1);
assert_eq!(dur.seconds, 5);
assert_eq!(dur.months, 0); }
#[test]
fn test_duration_ambiguous_m() {
let dur = IsoDuration::parse("P1MT1M").unwrap();
assert_eq!(dur.months, 1);
assert_eq!(dur.minutes, 1);
}
#[test]
fn test_duration_formatting() {
let mut dur = IsoDuration::default();
dur.years = 1;
dur.hours = 2;
assert_eq!(dur.to_string(), "P1YT2H");
}
}