pub fn parse_iso_timestamp(s: &str) -> Option<chrono::NaiveDateTime> {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
return Some(dt.naive_utc());
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.fZ") {
return Some(dt);
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%SZ") {
return Some(dt);
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%m/%d/%Y %H:%M:%S") {
return Some(dt);
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%m/%d/%Y %I:%M:%S %p") {
return Some(dt);
}
if let Ok(d) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
return Some(d.and_hms_opt(0, 0, 0).unwrap());
}
None
}
pub fn month_bounds(year: i32, month: u32) -> (chrono::NaiveDate, chrono::NaiveDate) {
let start = chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap();
let end = if month == 12 {
chrono::NaiveDate::from_ymd_opt(year + 1, 1, 1)
.unwrap()
.pred_opt()
.unwrap()
} else {
chrono::NaiveDate::from_ymd_opt(year, month + 1, 1)
.unwrap()
.pred_opt()
.unwrap()
};
(start, end)
}
pub fn year_bounds(year: i32) -> (chrono::NaiveDate, chrono::NaiveDate) {
let start = chrono::NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(year, 12, 31).unwrap();
(start, end)
}
pub fn date_str_in_range(date_str: &str, start: chrono::NaiveDate, end: chrono::NaiveDate) -> bool {
if let Ok(d) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
d >= start && d <= end
} else {
false
}
}
pub fn timestamp_in_range_fast(line: &str, start: chrono::NaiveDate, end: chrono::NaiveDate) -> bool {
if let Some(idx) = line.find("\"timestamp\":\"") {
let after = &line[idx + 13..];
if after.len() >= 10 {
let date_str = &after[..10];
return date_str_in_range(date_str, start, end);
}
}
if let Some(idx) = line.find("\"timestamp\": \"") {
let after = &line[idx + 14..];
if after.len() >= 10 {
let date_str = &after[..10];
return date_str_in_range(date_str, start, end);
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Timelike;
#[test]
fn test_parse_iso_timestamp_full() {
let dt = parse_iso_timestamp("2025-11-04T08:34:16.259Z");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.date().to_string(), "2025-11-04");
assert_eq!(dt.time().hour(), 8);
}
#[test]
fn test_parse_iso_timestamp_date_only() {
let dt = parse_iso_timestamp("2024-03-15");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.date().to_string(), "2024-03-15");
}
#[test]
fn test_parse_iso_timestamp_invalid() {
assert!(parse_iso_timestamp("not-a-date").is_none());
assert!(parse_iso_timestamp("").is_none());
}
#[test]
fn test_parse_iso_timestamp_without_ms() {
let dt = parse_iso_timestamp("2026-04-15T10:30:00Z");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.date().to_string(), "2026-04-15");
assert_eq!(dt.time().hour(), 10);
}
#[test]
fn test_parse_iso_timestamp_with_timezone_offset() {
let dt = parse_iso_timestamp("2026-04-15T10:30:00+08:00");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.date().to_string(), "2026-04-15");
}
#[test]
fn test_parse_localized_timestamp() {
let dt = parse_iso_timestamp("03/09/2026 14:00:30");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.date().to_string(), "2026-03-09");
assert_eq!(dt.time().hour(), 14);
assert_eq!(dt.time().minute(), 0);
assert_eq!(dt.time().second(), 30);
}
#[test]
fn test_parse_localized_timestamp_invalid() {
assert!(parse_iso_timestamp("13/32/2026 25:00:00").is_none());
}
#[test]
fn test_month_bounds() {
let (start, end) = month_bounds(2026, 1);
assert_eq!(start.to_string(), "2026-01-01");
assert_eq!(end.to_string(), "2026-01-31");
let (start, end) = month_bounds(2026, 2);
assert_eq!(start.to_string(), "2026-02-01");
assert_eq!(end.to_string(), "2026-02-28");
let (_start, end) = month_bounds(2024, 2); assert_eq!(end.to_string(), "2024-02-29");
}
#[test]
fn test_month_bounds_december() {
let (start, end) = month_bounds(2026, 12);
assert_eq!(start.to_string(), "2026-12-01");
assert_eq!(end.to_string(), "2026-12-31");
}
#[test]
fn test_year_bounds() {
let (start, end) = year_bounds(2026);
assert_eq!(start.to_string(), "2026-01-01");
assert_eq!(end.to_string(), "2026-12-31");
}
#[test]
fn test_year_bounds_leap_year() {
let (start, end) = year_bounds(2024);
assert_eq!(start.to_string(), "2024-01-01");
assert_eq!(end.to_string(), "2024-12-31");
}
#[test]
fn test_date_str_in_range() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 4, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 4, 30).unwrap();
assert!(date_str_in_range("2026-04-15", start, end));
assert!(date_str_in_range("2026-04-01", start, end));
assert!(date_str_in_range("2026-04-30", start, end));
assert!(!date_str_in_range("2026-03-31", start, end));
assert!(!date_str_in_range("2026-05-01", start, end));
assert!(!date_str_in_range("bad-date", start, end));
}
#[test]
fn test_date_str_in_range_boundary() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 1, 31).unwrap();
assert!(date_str_in_range("2026-01-01", start, end));
assert!(date_str_in_range("2026-01-31", start, end));
assert!(!date_str_in_range("2025-12-31", start, end));
assert!(!date_str_in_range("2026-02-01", start, end));
}
#[test]
fn test_timestamp_in_range_fast() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 4, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 4, 30).unwrap();
let line = r#"{"type":"assistant","timestamp":"2026-04-15T10:30:00Z","message":{}}"#;
assert!(timestamp_in_range_fast(line, start, end));
let line = r#"{"type":"assistant","timestamp":"2026-03-01T10:30:00Z","message":{}}"#;
assert!(!timestamp_in_range_fast(line, start, end));
let line = r#"{"type":"assistant","no_timestamp":"here"}"#;
assert!(!timestamp_in_range_fast(line, start, end));
}
#[test]
fn test_timestamp_in_range_fast_boundary() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 4, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 4, 30).unwrap();
let line = r#"{"timestamp":"2026-04-01T00:00:00Z","type":"assistant"}"#;
assert!(timestamp_in_range_fast(line, start, end));
let line = r#"{"timestamp":"2026-04-30T23:59:59Z","type":"assistant"}"#;
assert!(timestamp_in_range_fast(line, start, end));
let line = r#"{"timestamp":"2026-03-31T23:59:59Z","type":"assistant"}"#;
assert!(!timestamp_in_range_fast(line, start, end));
}
#[test]
fn test_timestamp_in_range_fast_with_space_after_colon() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 4, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 4, 30).unwrap();
let line = r#"{"timestamp": "2026-04-15T10:30:00Z", "type": "assistant"}"#;
assert!(timestamp_in_range_fast(line, start, end));
}
#[test]
fn test_timestamp_in_range_fast_line_without_date() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 4, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 4, 30).unwrap();
let line = r#"{"timestamp":"bad"}"#;
assert!(!timestamp_in_range_fast(line, start, end));
}
#[test]
fn test_timestamp_in_range_fast_empty_line() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 12, 31).unwrap();
assert!(!timestamp_in_range_fast("", start, end));
}
#[test]
fn test_timestamp_in_range_fast_short_timestamp() {
let start = chrono::NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 12, 31).unwrap();
let line = r#"{"timestamp":"2026-0"}"#;
assert!(!timestamp_in_range_fast(line, start, end));
}
}