use crate::date::formats::{FormatSpec, POSSIBLE_FORMATS};
use crate::DateKind;
use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime};
use regex::Regex;
use std::sync::OnceLock;
fn date_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
#[allow(clippy::expect_used)]
Regex::new(r"([0-9][0-9\-_\.]+[0-9])").expect("static date regex compiles")
})
}
#[must_use]
pub fn try_date(candidate: &str, current_year: i32) -> Option<(String, DateKind)> {
for spec in POSSIBLE_FORMATS {
if spec.template.len() != candidate.len() {
continue;
}
if let Some((dt, kind)) = parse_with_template(candidate, spec, current_year) {
return Some((format_iso(dt, kind), kind));
}
}
None
}
fn parse_with_template(
candidate: &str,
spec: &FormatSpec,
current_year: i32,
) -> Option<(NaiveDateTime, DateKind)> {
let parts = split_by_template(candidate, spec.template)?;
let year = parts.year?;
let mut year = year as i32;
if !spec.has_century {
year += 2000;
let next_ten = current_year + 10;
if year > next_ten {
year -= 100;
}
}
let month = parts.month.unwrap_or(1);
let day = parts.day.unwrap_or(1);
let hour = parts.hour.unwrap_or(0);
let minute = parts.minute.unwrap_or(0);
let second = parts.second.unwrap_or(0);
let date = NaiveDate::from_ymd_opt(year, month, day)?;
let time = NaiveTime::from_hms_opt(hour, minute, second)?;
Some((NaiveDateTime::new(date, time), spec.kind))
}
#[derive(Default)]
struct ParsedParts {
year: Option<u32>,
month: Option<u32>,
day: Option<u32>,
hour: Option<u32>,
minute: Option<u32>,
second: Option<u32>,
}
fn split_by_template(candidate: &str, template: &str) -> Option<ParsedParts> {
let cb = candidate.as_bytes();
let tb = template.as_bytes();
let mut parts = ParsedParts::default();
let mut ci = 0usize;
let mut ti = 0usize;
while ti < tb.len() {
let t = tb[ti];
if t == b'Y' {
let n = run_len(tb, ti, b'Y');
let raw = read_digits(cb, ci, n)?;
ci += n;
ti += n;
parts.year = Some(raw);
} else if t == b'M' {
let n = run_len(tb, ti, b'M');
let raw = read_digits(cb, ci, n)?;
ci += n;
ti += n;
parts.month = Some(raw);
} else if t == b'D' {
let n = run_len(tb, ti, b'D');
let raw = read_digits(cb, ci, n)?;
ci += n;
ti += n;
parts.day = Some(raw);
} else if t == b'H' {
let n = run_len(tb, ti, b'H');
let raw = read_digits(cb, ci, n)?;
ci += n;
ti += n;
parts.hour = Some(raw);
} else if t == b'm' {
let n = run_len(tb, ti, b'm');
let raw = read_digits(cb, ci, n)?;
ci += n;
ti += n;
parts.minute = Some(raw);
} else if t == b's' {
let n = run_len(tb, ti, b's');
let raw = read_digits(cb, ci, n)?;
ci += n;
ti += n;
parts.second = Some(raw);
} else {
if ci >= cb.len() || cb[ci] != t {
return None;
}
ci += 1;
ti += 1;
}
}
if ci != cb.len() {
return None;
}
Some(parts)
}
fn run_len(bytes: &[u8], start: usize, ch: u8) -> usize {
let mut n = 0;
while start + n < bytes.len() && bytes[start + n] == ch {
n += 1;
}
n
}
fn read_digits(bytes: &[u8], start: usize, n: usize) -> Option<u32> {
if start + n > bytes.len() {
return None;
}
let slice = &bytes[start..start + n];
if !slice.iter().all(u8::is_ascii_digit) {
return None;
}
#[allow(clippy::expect_used)]
let s = std::str::from_utf8(slice).expect("ASCII digits are valid UTF-8");
s.parse().ok()
}
fn format_iso(dt: NaiveDateTime, kind: DateKind) -> String {
match kind {
DateKind::MonthOnly => format!("{:04}-{:02}", dt.year(), dt.month()),
DateKind::DateOnly => dt.format("%Y-%m-%d").to_string(),
DateKind::DateTime => dt.format("%Y-%m-%dT%H-%M-%S").to_string(),
}
}
#[must_use]
pub fn detect_and_replace(slugged: &str, internal_sep: char, current_year: i32) -> String {
let _ = internal_sep; date_regex()
.replace_all(slugged, |caps: ®ex::Captures<'_>| {
#[allow(clippy::expect_used)]
let candidate = caps.get(0).expect("regex group 0").as_str();
match try_date(candidate, current_year) {
Some((iso, _)) => format!("_{iso}_"),
None => candidate.to_string(),
}
})
.to_string()
}