pub(crate) fn is_valid_duration(s: &str) -> bool {
let s = if let Some(stripped) = s.strip_prefix('-') {
stripped
} else {
s
};
if !s.starts_with('P') || s.len() < 2 {
return false;
}
let rest = &s[1..];
let (date_part, time_part) = if let Some(t_pos) = rest.find('T') {
(&rest[..t_pos], Some(&rest[t_pos + 1..]))
} else {
(rest, None)
};
let mut has_any_component = false;
let mut remaining = date_part;
for designator in ['Y', 'M', 'D'] {
if let Some(pos) = remaining.find(designator) {
let num = &remaining[..pos];
if num.is_empty() || !num.chars().all(|c| c.is_ascii_digit()) {
return false;
}
has_any_component = true;
remaining = &remaining[pos + 1..];
}
}
if !remaining.is_empty() {
return false;
}
if let Some(tp) = time_part {
if tp.is_empty() {
return false; }
let mut remaining = tp;
let mut has_time_component = false;
for designator in ['H', 'M', 'S'] {
if let Some(pos) = remaining.find(designator) {
let num = &remaining[..pos];
if num.is_empty() {
return false;
}
if designator == 'S' {
let parts: Vec<&str> = num.split('.').collect();
if parts.len() > 2 {
return false;
}
if !parts[0].chars().all(|c| c.is_ascii_digit()) || parts[0].is_empty() {
return false;
}
if parts.len() == 2
&& (!parts[1].chars().all(|c| c.is_ascii_digit()) || parts[1].is_empty())
{
return false;
}
} else if !num.chars().all(|c| c.is_ascii_digit()) {
return false;
}
has_time_component = true;
remaining = &remaining[pos + 1..];
}
}
if !remaining.is_empty() || !has_time_component {
return false;
}
has_any_component = true;
}
has_any_component
}
pub(crate) fn is_valid_gyear(s: &str) -> bool {
let s = strip_timezone(s);
let s = if let Some(stripped) = s.strip_prefix('-') {
stripped
} else {
s
};
s.len() >= 4 && s.chars().all(|c| c.is_ascii_digit())
}
pub(crate) fn is_valid_gyearmonth(s: &str) -> bool {
let s = strip_timezone(s);
let (s, _neg) = if let Some(stripped) = s.strip_prefix('-') {
(stripped, true)
} else {
(s, false)
};
if let Some(dash_pos) = s.rfind('-') {
if dash_pos < 4 {
return false;
}
let year = &s[..dash_pos];
let month = &s[dash_pos + 1..];
if year.len() < 4 || !year.chars().all(|c| c.is_ascii_digit()) {
return false;
}
if month.len() != 2 || !month.chars().all(|c| c.is_ascii_digit()) {
return false;
}
if let Ok(m) = month.parse::<u32>() {
(1..=12).contains(&m)
} else {
false
}
} else {
false
}
}
pub(crate) fn is_valid_gmonth(s: &str) -> bool {
let s = strip_timezone(s);
if !s.starts_with("--") || s.len() < 4 {
return false;
}
let month_str = &s[2..4];
if !month_str.chars().all(|c| c.is_ascii_digit()) {
return false;
}
let rest = &s[4..];
if !rest.is_empty() && rest != "--" {
return false;
}
if let Ok(m) = month_str.parse::<u32>() {
(1..=12).contains(&m)
} else {
false
}
}
fn max_days_for_month(month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => 29,
_ => 0,
}
}
fn max_days_for_month_year(month: u32, year: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
fn is_leap_year(year: u32) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
pub(crate) fn is_valid_gmonthday(s: &str) -> bool {
let s = strip_timezone(s);
if !s.starts_with("--") || s.len() < 7 {
return false;
}
let month_str = &s[2..4];
if s.as_bytes()[4] != b'-' {
return false;
}
let day_str = &s[5..7];
if !month_str.chars().all(|c| c.is_ascii_digit())
|| !day_str.chars().all(|c| c.is_ascii_digit())
{
return false;
}
if s.len() != 7 {
return false;
}
let month = match month_str.parse::<u32>() {
Ok(m) if (1..=12).contains(&m) => m,
_ => return false,
};
let day = match day_str.parse::<u32>() {
Ok(d) if d >= 1 => d,
_ => return false,
};
day <= max_days_for_month(month)
}
pub(crate) fn is_valid_gday(s: &str) -> bool {
let s = strip_timezone(s);
if !s.starts_with("---") || s.len() < 5 {
return false;
}
let day_str = &s[3..5];
if day_str.len() != 2 || !day_str.chars().all(|c| c.is_ascii_digit()) {
return false;
}
if s.len() != 5 {
return false;
}
if let Ok(d) = day_str.parse::<u32>() {
(1..=31).contains(&d)
} else {
false
}
}
pub(crate) fn normalize_datetime_tz(s: &str) -> String {
let mut val = String::from(s);
if val.ends_with("+00:00") || val.ends_with("-00:00") {
let end = val.len() - 6;
val.truncate(end);
val.push('Z');
}
if let Some(dot_pos) = val.rfind('.') {
let after_dot = &val[dot_pos + 1..];
let frac_end = after_dot
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after_dot.len());
let frac = &after_dot[..frac_end];
let trimmed_frac = frac.trim_end_matches('0');
if trimmed_frac.is_empty() {
let suffix = &after_dot[frac_end..];
let mut new = val[..dot_pos].to_string();
new.push_str(suffix);
val = new;
} else if trimmed_frac.len() < frac.len() {
let suffix = &after_dot[frac_end..];
let mut new = val[..dot_pos + 1].to_string();
new.push_str(trimmed_frac);
new.push_str(suffix);
val = new;
}
}
val
}
pub(crate) fn is_valid_datetime(s: &str) -> bool {
if let Some(t_pos) = s.find('T') {
let date_part = &s[..t_pos];
let time_part = &s[t_pos + 1..];
is_valid_date(date_part) && is_valid_time(time_part)
} else {
false
}
}
pub(crate) fn is_valid_date(s: &str) -> bool {
let s = strip_timezone(s);
let parts: Vec<&str> = s.split('-').collect();
if s.starts_with('-') {
if parts.len() < 4 {
return false;
}
if parts[1].len() < 4 || !parts[1].chars().all(|c| c.is_ascii_digit()) {
return false;
}
if parts[2].len() != 2 || parts[3].len() != 2 {
return false;
}
let year: u32 = match parts[1].parse() {
Ok(y) => y,
Err(_) => return false,
};
let month: u32 = match parts[2].parse() {
Ok(m) => m,
Err(_) => return false,
};
let day: u32 = match parts[3].parse() {
Ok(d) => d,
Err(_) => return false,
};
if year == 0 {
return false;
}
if !(1..=12).contains(&month) {
return false;
}
if day < 1 || day > max_days_for_month_year(month, year) {
return false;
}
return true;
}
if parts.len() != 3 {
return false;
}
if parts[0].len() < 4
|| !parts[0].chars().all(|c| c.is_ascii_digit())
|| parts[1].len() != 2
|| !parts[1].chars().all(|c| c.is_ascii_digit())
|| parts[2].len() != 2
|| !parts[2].chars().all(|c| c.is_ascii_digit())
{
return false;
}
let year: u32 = match parts[0].parse() {
Ok(y) => y,
Err(_) => return false,
};
let month: u32 = match parts[1].parse() {
Ok(m) => m,
Err(_) => return false,
};
let day: u32 = match parts[2].parse() {
Ok(d) => d,
Err(_) => return false,
};
if year == 0 {
return false;
}
if !(1..=12).contains(&month) {
return false;
}
if day < 1 || day > max_days_for_month_year(month, year) {
return false;
}
true
}
pub(crate) fn is_valid_time(s: &str) -> bool {
let s = strip_time_timezone(s);
let parts: Vec<&str> = s.split(':').collect();
if parts.len() < 3 {
return false;
}
let seconds_parts: Vec<&str> = parts[2].split('.').collect();
if parts[0].len() != 2
|| parts[1].len() != 2
|| seconds_parts[0].len() != 2
|| !parts[0].chars().all(|c| c.is_ascii_digit())
|| !parts[1].chars().all(|c| c.is_ascii_digit())
|| !seconds_parts[0].chars().all(|c| c.is_ascii_digit())
{
return false;
}
let hours: u32 = match parts[0].parse() {
Ok(h) => h,
Err(_) => return false,
};
let minutes: u32 = match parts[1].parse() {
Ok(m) => m,
Err(_) => return false,
};
let seconds: u32 = match seconds_parts[0].parse() {
Ok(s) => s,
Err(_) => return false,
};
if hours == 24 {
return minutes == 0 && seconds == 0;
}
hours <= 23 && minutes <= 59 && seconds <= 59
}
fn strip_time_timezone(s: &str) -> &str {
if let Some(stripped) = s.strip_suffix('Z') {
return stripped;
}
if s.len() >= 6 {
let tz_start = s.len() - 6;
let c = s.as_bytes()[tz_start];
if (c == b'+' || c == b'-') && s.as_bytes()[tz_start + 3] == b':' {
return &s[..tz_start];
}
}
s
}
pub(crate) fn strip_timezone(s: &str) -> &str {
if let Some(stripped) = s.strip_suffix('Z') {
return stripped;
}
if s.len() >= 6 {
let tz_start = s.len() - 6;
let b = s.as_bytes();
if (b[tz_start] == b'+' || b[tz_start] == b'-')
&& b[tz_start + 1].is_ascii_digit()
&& b[tz_start + 2].is_ascii_digit()
&& b[tz_start + 3] == b':'
&& b[tz_start + 4].is_ascii_digit()
&& b[tz_start + 5].is_ascii_digit()
{
return &s[..tz_start];
}
}
s
}