use crate::Time;
use jiff::Zoned;
pub fn parse_git_date_format(input: &str) -> Option<Time> {
parse_iso8601_dots(input)
.or_else(|| parse_compact_iso8601(input))
.or_else(|| parse_flexible_iso8601(input))
}
fn parse_iso8601_dots(input: &str) -> Option<Time> {
let input = input.trim();
let first_10 = input.get(..10)?;
if !first_10.is_ascii() || !first_10.contains('.') {
return None;
}
let (date_part, rest) = input.split_once(' ')?;
if date_part.len() != 10 || date_part.chars().nth(4)? != '.' || date_part.chars().nth(7)? != '.' {
return None;
}
let normalized = format!("{} {}", date_part.replace('.', "-"), rest);
parse_flexible_iso8601(&normalized)
}
fn parse_compact_iso8601(input: &str) -> Option<Time> {
let input = input.trim();
let t_pos = input.find('T')?;
if t_pos != 8 {
return None;
}
let date_part = &input.get(..8)?;
if !date_part.is_ascii() {
return None;
}
let year: i32 = date_part[0..4].parse().ok()?;
let month: i32 = date_part[4..6].parse().ok()?;
let day: i32 = date_part[6..8].parse().ok()?;
let rest = &input.get(9..)?; let (time_str, offset_str) = split_time_and_offset(rest);
let time_str = if let Some(dot_pos) = time_str.find('.') {
&time_str[..dot_pos]
} else {
time_str
};
let (hour, minute, second) = parse_time_component(time_str)?;
let offset = parse_flexible_offset(offset_str)?;
let zoned = new_zoned(year, month, day, hour, minute, second, offset)?;
Time::new(zoned.timestamp().as_second(), offset).into()
}
fn parse_flexible_iso8601(input: &str) -> Option<Time> {
let input = input.trim();
let date_part = &input.get(..10)?;
if !date_part.is_ascii() {
return None;
}
if date_part.chars().nth(4)? != '-' || date_part.chars().nth(7)? != '-' {
return None;
}
let year: i32 = date_part[0..4].parse().ok()?;
let month: i32 = date_part[5..7].parse().ok()?;
let day: i32 = date_part[8..10].parse().ok()?;
let rest = &input.get(10..)?;
if rest.is_empty() {
return None;
}
let rest = if rest.starts_with('T') || rest.starts_with(' ') {
&rest[1..]
} else {
return None;
};
let (time_str, offset_str) = split_time_and_offset(rest);
let time_str = if let Some(dot_pos) = time_str.find('.') {
&time_str[..dot_pos]
} else {
time_str
};
let (hour, minute, second) = parse_time_component(time_str)?;
let offset = parse_flexible_offset(offset_str)?;
let zoned = new_zoned(year, month, day, hour, minute, second, offset)?;
Some(Time::new(zoned.timestamp().as_second(), offset))
}
fn new_zoned(year: i32, month: i32, day: i32, hour: u32, minute: u32, second: u32, offset: i32) -> Option<Zoned> {
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let date = jiff::civil::Date::new(year as i16, month as i8, day as i8).ok()?;
let datetime = date.at(hour as i8, minute as i8, second as i8, 0);
let tz_offset = jiff::tz::Offset::from_seconds(offset).ok()?;
let zoned = datetime.to_zoned(tz_offset.to_time_zone()).ok()?;
zoned.into()
}
fn split_time_and_offset(input: &str) -> (&str, &str) {
let input = input.trim();
if let Some(stripped) = input.strip_suffix('Z') {
return (stripped, "Z");
}
let mut offset_start = None;
for (i, c) in input.char_indices().rev() {
if (c == '+' || c == '-') && i >= 5 {
let after = &input[i + 1..];
if after.chars().next().is_some_and(|c| c.is_ascii_digit()) {
offset_start = Some(i);
break;
}
}
}
if let Some(space_pos) = input.rfind(' ') {
if space_pos > 5 {
let potential_offset = input[space_pos + 1..].trim();
if potential_offset.starts_with('+') || potential_offset.starts_with('-') || potential_offset == "Z" {
return (&input[..space_pos], potential_offset);
}
}
}
if let Some(pos) = offset_start {
(&input[..pos], &input[pos..])
} else {
(input, "")
}
}
fn parse_time_component(time: &str) -> Option<(u32, u32, u32)> {
let time = time.trim();
if !time.is_ascii() {
return None;
}
let (hour, minute, second) = if time.contains(':') {
let parts: Vec<&str> = time.split(':').collect();
let hour: u32 = parts.first()?.parse().ok()?;
let minute: u32 = parts.get(1).unwrap_or(&"0").parse().ok()?;
let second: u32 = parts.get(2).unwrap_or(&"0").parse().ok()?;
Some((hour, minute, second))
} else {
match time.len() {
2 => {
let hour: u32 = time.parse().ok()?;
Some((hour, 0, 0))
}
4 => {
let hour: u32 = time[0..2].parse().ok()?;
let minute: u32 = time[2..4].parse().ok()?;
Some((hour, minute, 0))
}
6 => {
let hour: u32 = time[0..2].parse().ok()?;
let minute: u32 = time[2..4].parse().ok()?;
let second: u32 = time[4..6].parse().ok()?;
Some((hour, minute, second))
}
_ => None,
}
}?;
if hour > 23 || minute > 59 || second > 59 {
return None;
}
(hour, minute, second).into()
}
fn parse_flexible_offset(offset: &str) -> Option<i32> {
let offset = offset.trim();
if offset.is_empty() {
return Some(0);
}
if !offset.is_ascii() {
return None;
}
if offset == "Z" {
return Some(0);
}
let (sign, rest) = if let Some(stripped) = offset.strip_prefix('+') {
(1, stripped)
} else if let Some(stripped) = offset.strip_prefix('-') {
(-1, stripped)
} else {
return None;
};
let rest = rest.replace(':', "");
let (hours, minutes) = match rest.len() {
2 => {
let hours: i32 = rest.parse().ok()?;
(hours, 0)
}
4 => {
let hours: i32 = rest[0..2].parse().ok()?;
let minutes: i32 = rest[2..4].parse().ok()?;
(hours, minutes)
}
_ => return None,
};
if hours > 23 || minutes > 59 {
return None;
}
Some(sign * (hours * 3600 + minutes * 60))
}