use chrono::{DateTime, Datelike, Local, TimeZone, Utc};
use std::cell::RefCell;
pub struct AdaptiveTsParser {
formats: Vec<String>,
}
impl AdaptiveTsParser {
pub fn new() -> Self {
Self {
formats: get_initial_timestamp_formats(),
}
}
pub fn parse_ts_with_config(
&mut self,
ts_str: &str,
custom_format: Option<&str>,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
let ts_str = ts_str.trim();
if let Some(format) = custom_format {
if let Some(parsed) = try_parse_with_format(ts_str, format, default_timezone) {
return Some(parsed);
}
}
match ts_str {
"now" => return Some(Utc::now()),
"today" => {
let local_today = Local::now().date_naive();
if let Some(naive_datetime) = local_today.and_hms_opt(0, 0, 0) {
return Some(naive_datetime.and_utc());
}
}
"yesterday" => {
let local_yesterday = Local::now().date_naive() - chrono::Duration::days(1);
if let Some(naive_datetime) = local_yesterday.and_hms_opt(0, 0, 0) {
return Some(naive_datetime.and_utc());
}
}
"tomorrow" => {
let local_tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
if let Some(naive_datetime) = local_tomorrow.and_hms_opt(0, 0, 0) {
return Some(naive_datetime.and_utc());
}
}
_ => {}
}
if ts_str.starts_with('-') || ts_str.starts_with('+') || looks_like_relative_time(ts_str) {
if let Ok(dt) = parse_relative_time(ts_str) {
return Some(dt);
}
}
if looks_like_unix_timestamp(ts_str) {
if let Some(parsed) = try_parse_unix_timestamp(ts_str) {
return Some(parsed);
}
}
if let Ok(dt) = DateTime::parse_from_rfc3339(ts_str) {
return Some(dt.with_timezone(&Utc));
}
if let Ok(dt) = DateTime::parse_from_rfc2822(ts_str) {
return Some(dt.with_timezone(&Utc));
}
if let Some(dt) = parse_date_only(ts_str) {
return Some(dt);
}
if let Some(dt) = parse_time_only(ts_str) {
return Some(dt);
}
self.try_formats_with_reordering(ts_str, default_timezone)
}
fn try_formats_with_reordering(
&mut self,
ts_str: &str,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
for (index, format) in self.formats.iter().enumerate() {
if let Some(parsed) = try_parse_with_format(ts_str, format, default_timezone) {
if index > 0 {
let successful_format = self.formats.remove(index);
self.formats.insert(0, successful_format);
}
return Some(parsed);
}
}
None
}
}
thread_local! {
static THREAD_TS_PARSER: RefCell<AdaptiveTsParser> =
RefCell::new(AdaptiveTsParser::new());
}
pub fn with_thread_local_parser<R>(f: impl FnOnce(&mut AdaptiveTsParser) -> R) -> R {
THREAD_TS_PARSER.with(|parser| f(&mut parser.borrow_mut()))
}
#[cfg(test)]
impl AdaptiveTsParser {
pub fn parse_ts(&mut self, ts_str: &str) -> Option<chrono::DateTime<chrono::Utc>> {
self.parse_ts_with_config(ts_str, None, None)
}
pub fn reset_ordering(&mut self) {
self.formats = get_initial_timestamp_formats();
}
pub fn get_format_ordering(&self) -> Vec<String> {
self.formats.clone()
}
}
impl Default for AdaptiveTsParser {
fn default() -> Self {
Self::new()
}
}
fn apply_timezone_to_naive(
naive_dt: chrono::NaiveDateTime,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
use chrono_tz::Tz;
match default_timezone {
Some("UTC") => Some(naive_dt.and_utc()),
Some(tz_str) => {
if let Ok(tz) = tz_str.parse::<Tz>() {
if let Some(dt) = tz.from_local_datetime(&naive_dt).single() {
return Some(dt.with_timezone(&Utc));
}
}
chrono::Local
.from_local_datetime(&naive_dt)
.single()
.map(|local_dt| local_dt.with_timezone(&Utc))
}
None => {
chrono::Local
.from_local_datetime(&naive_dt)
.single()
.map(|local_dt| local_dt.with_timezone(&Utc))
}
}
}
fn choose_best_timestamp(candidates: &[DateTime<Utc>], now: DateTime<Utc>) -> DateTime<Utc> {
use chrono::Duration;
const FUTURE_TOLERANCE_HOURS: i64 = 24;
let future_cutoff = now + Duration::hours(FUTURE_TOLERANCE_HOURS);
let reasonable: Vec<_> = candidates
.iter()
.filter(|dt| **dt <= future_cutoff)
.collect();
let candidates_to_use = if !reasonable.is_empty() {
reasonable
} else {
candidates.iter().collect()
};
**candidates_to_use
.iter()
.min_by_key(|dt| {
let diff = ***dt - now;
diff.num_seconds().abs()
})
.expect("candidates should not be empty")
}
fn try_parse_with_format(
ts_str: &str,
format: &str,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
let ts_str = ts_str.trim();
let ts_str = if ts_str.starts_with('[') && ts_str.ends_with(']') {
&ts_str[1..ts_str.len() - 1]
} else {
ts_str
};
let (processed_ts_str, processed_format) = if format.contains(",%f") {
if let Some(comma_pos) = ts_str.rfind(',') {
let base_part = &ts_str[..comma_pos];
let frac_part = &ts_str[comma_pos + 1..];
if !frac_part.is_empty() && frac_part.chars().all(|c| c.is_ascii_digit()) {
let frac_nanos = if frac_part.len() <= 3 {
format!("{:0<9}", format!("{:0<3}", frac_part))
} else if frac_part.len() <= 6 {
format!("{:0<9}", format!("{:0<6}", frac_part))
} else {
let mut truncated: String = frac_part.chars().take(9).collect();
if truncated.len() < 9 {
truncated = format!("{:0<9}", truncated);
}
truncated
};
let new_ts_str = format!("{}.{}", base_part, frac_nanos);
let new_format = format.replace(",%f", ".%f");
(new_ts_str, new_format)
} else {
(ts_str.to_string(), format.to_string())
}
} else {
(ts_str.to_string(), format.to_string())
}
} else {
(ts_str.to_string(), format.to_string())
};
if let Ok(dt) = DateTime::parse_from_str(&processed_ts_str, &processed_format) {
return Some(dt.with_timezone(&Utc));
}
let has_month = processed_format.contains("%b") || processed_format.contains("%B") || processed_format.contains("%m"); let has_day = processed_format.contains("%d") || processed_format.contains("%e"); let has_year = processed_format.contains("%Y") || processed_format.contains("%y");
if has_month && has_day && !has_year {
let now = chrono::Utc::now();
let current_year = now.year();
let format_with_year = format!("%Y {}", processed_format);
let mut candidates = Vec::new();
for year_offset in [-1, 0, 1] {
let year = current_year + year_offset;
let ts_with_year = format!("{} {}", year, processed_ts_str);
if let Ok(naive_dt) =
chrono::NaiveDateTime::parse_from_str(&ts_with_year, &format_with_year)
{
if let Some(dt) = apply_timezone_to_naive(naive_dt, default_timezone) {
candidates.push(dt);
}
}
}
if !candidates.is_empty() {
crate::stats::stats_add_yearless_timestamp();
return Some(choose_best_timestamp(&candidates, now));
}
}
if let Ok(naive_dt) =
chrono::NaiveDateTime::parse_from_str(&processed_ts_str, &processed_format)
{
return apply_timezone_to_naive(naive_dt, default_timezone);
}
None
}
fn looks_like_unix_timestamp(ts_str: &str) -> bool {
if ts_str.is_empty() {
return false;
}
let mut has_dot = false;
for c in ts_str.chars() {
if c == '.' {
if has_dot {
return false; }
has_dot = true;
} else if !c.is_ascii_digit() {
return false;
}
}
ts_str.chars().next().is_some_and(|c| c.is_ascii_digit())
}
fn try_parse_unix_timestamp(ts_str: &str) -> Option<DateTime<Utc>> {
if ts_str.contains('.') {
if let Ok(timestamp_float) = ts_str.parse::<f64>() {
return try_parse_unix_float(timestamp_float);
}
} else {
if let Ok(timestamp_int) = ts_str.parse::<i64>() {
return try_parse_unix_int(ts_str.len(), timestamp_int);
}
}
None
}
fn try_parse_unix_float(timestamp_float: f64) -> Option<DateTime<Utc>> {
let dt = if timestamp_float >= 1e15 {
DateTime::from_timestamp(
(timestamp_float / 1_000_000.0).floor() as i64,
((timestamp_float % 1_000_000.0) * 1000.0) as u32,
)
} else if timestamp_float >= 1e12 {
DateTime::from_timestamp(
(timestamp_float / 1000.0).floor() as i64,
((timestamp_float % 1000.0) * 1_000_000.0) as u32,
)
} else if timestamp_float >= 1e9 {
DateTime::from_timestamp(
timestamp_float.floor() as i64,
(timestamp_float.fract() * 1_000_000_000.0) as u32,
)
} else {
None
};
dt.map(|dt| dt.with_timezone(&Utc))
}
fn try_parse_unix_int(str_len: usize, timestamp_int: i64) -> Option<DateTime<Utc>> {
let dt = match str_len {
10 => {
DateTime::from_timestamp(timestamp_int, 0)
}
13 => {
DateTime::from_timestamp(
timestamp_int / 1000,
(timestamp_int % 1000) as u32 * 1_000_000,
)
}
16 => {
DateTime::from_timestamp(
timestamp_int / 1_000_000,
(timestamp_int % 1_000_000) as u32 * 1_000,
)
}
19 => {
DateTime::from_timestamp(
timestamp_int / 1_000_000_000,
(timestamp_int % 1_000_000_000) as u32,
)
}
_ => None,
};
dt.map(|dt| dt.with_timezone(&Utc))
}
fn get_initial_timestamp_formats() -> Vec<String> {
vec![
"%Y-%m-%dT%H:%M:%S%.fZ".to_string(), "%Y-%m-%dT%H:%M:%SZ".to_string(), "%Y-%m-%dT%H:%M:%S%.f%:z".to_string(), "%Y-%m-%dT%H:%M:%S%:z".to_string(), "%Y-%m-%dT%H:%M:%S%.f".to_string(), "%Y-%m-%dT%H:%M:%S".to_string(), "%Y-%m-%d %H:%M:%S%.f".to_string(), "%Y-%m-%d %H:%M:%S".to_string(), "%Y-%m-%d %H:%M:%S%.fZ".to_string(), "%Y-%m-%d %H:%M:%SZ".to_string(), "%Y-%m-%d %H:%M:%S%z".to_string(), "%Y-%m-%d %H:%M:%S%.f%z".to_string(), "%b %d %H:%M:%S".to_string(), "%b %d %Y %H:%M:%S".to_string(), "%d/%b/%Y:%H:%M:%S %z".to_string(), "%Y-%m-%d %H:%M:%S,%f".to_string(), "%Y/%m/%d %H:%M:%S".to_string(), "%m/%d/%Y %H:%M:%S".to_string(), "%d.%m.%Y %H:%M:%S".to_string(), "%y%m%d %H:%M:%S".to_string(), "%d %b %Y, %H:%M".to_string(), "%a %b %d %H:%M:%S %Y".to_string(), "%d-%b-%y %I:%M:%S.%f %p".to_string(), "%b %d, %Y %I:%M:%S %p".to_string(), ]
}
#[derive(Debug, Clone, Default)]
pub struct TsConfig {
pub custom_field: Option<String>,
pub custom_format: Option<String>,
pub default_timezone: Option<String>,
}
pub fn identify_timestamp_field(
fields: &indexmap::IndexMap<String, rhai::Dynamic>,
config: &TsConfig,
) -> Option<(String, String)> {
if let Some(ref custom_field) = config.custom_field {
if let Some(value) = fields.get(custom_field) {
if let Some(ts_str) = dynamic_to_timestamp_string(value) {
return Some((custom_field.clone(), ts_str));
}
}
return None;
}
for ts_key in crate::event::TIMESTAMP_FIELD_NAMES {
if let Some(value) = fields.get(*ts_key) {
if let Some(ts_str) = dynamic_to_timestamp_string(value) {
return Some((ts_key.to_string(), ts_str));
}
}
}
None
}
fn dynamic_to_timestamp_string(value: &rhai::Dynamic) -> Option<String> {
if let Ok(int_val) = value.as_int() {
return Some(int_val.to_string());
}
if let Ok(float_val) = value.as_float() {
return Some(format!("{:.9}", float_val));
}
value.clone().into_string().ok()
}
pub fn parse_timestamp_arg_with_timezone(
arg: &str,
default_timezone: Option<&str>,
) -> Result<DateTime<Utc>, String> {
let mut parser = AdaptiveTsParser::new();
parser
.parse_ts_with_config(arg, None, default_timezone)
.ok_or_else(|| format!("Could not parse timestamp: {}", arg))
}
pub fn parse_anchored_timestamp(
arg: &str,
since_anchor: Option<DateTime<Utc>>,
until_anchor: Option<DateTime<Utc>>,
default_timezone: Option<&str>,
) -> Result<DateTime<Utc>, String> {
if let Some(offset_part) = arg.strip_prefix("since+") {
let since = since_anchor
.ok_or_else(|| "'since' anchor requires --since to be specified".to_string())?;
let duration = parse_duration(&format!("+{}", offset_part))?;
since
.checked_add_signed(duration)
.ok_or_else(|| "Anchored timestamp is out of supported range".to_string())
} else if let Some(offset_part) = arg.strip_prefix("since-") {
let since = since_anchor
.ok_or_else(|| "'since' anchor requires --since to be specified".to_string())?;
let duration = parse_duration(&format!("-{}", offset_part))?;
since
.checked_add_signed(duration)
.ok_or_else(|| "Anchored timestamp is out of supported range".to_string())
} else if let Some(offset_part) = arg.strip_prefix("until+") {
let until = until_anchor
.ok_or_else(|| "'until' anchor requires --until to be specified".to_string())?;
let duration = parse_duration(&format!("+{}", offset_part))?;
until
.checked_add_signed(duration)
.ok_or_else(|| "Anchored timestamp is out of supported range".to_string())
} else if let Some(offset_part) = arg.strip_prefix("until-") {
let until = until_anchor
.ok_or_else(|| "'until' anchor requires --until to be specified".to_string())?;
let duration = parse_duration(&format!("-{}", offset_part))?;
until
.checked_add_signed(duration)
.ok_or_else(|| "Anchored timestamp is out of supported range".to_string())
} else if let Some(offset_part) = arg.strip_prefix("now+") {
let duration = parse_duration(&format!("+{}", offset_part))?;
Utc::now()
.checked_add_signed(duration)
.ok_or_else(|| "Anchored timestamp is out of supported range".to_string())
} else if let Some(offset_part) = arg.strip_prefix("now-") {
let duration = parse_duration(&format!("-{}", offset_part))?;
Utc::now()
.checked_add_signed(duration)
.ok_or_else(|| "Anchored timestamp is out of supported range".to_string())
} else {
parse_timestamp_arg_with_timezone(arg, default_timezone)
}
}
fn looks_like_relative_time(arg: &str) -> bool {
if !arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return false;
}
let num_end = arg.find(|c: char| !c.is_ascii_digit()).unwrap_or(arg.len());
let remainder = &arg[num_end..].trim_start();
if remainder.is_empty() {
return false;
}
matches!(
*remainder,
"s" | "sec"
| "secs"
| "second"
| "seconds"
| "m"
| "min"
| "mins"
| "minute"
| "minutes"
| "h"
| "hour"
| "hours"
| "d"
| "day"
| "days"
| "w"
| "week"
| "weeks"
)
}
fn parse_duration(arg: &str) -> Result<chrono::Duration, String> {
let (sign, rest) = if let Some(stripped) = arg.strip_prefix('-') {
(-1, stripped)
} else if let Some(stripped) = arg.strip_prefix('+') {
(1, stripped)
} else {
(-1, arg)
};
if rest.is_empty() {
return Err("Empty relative time".to_string());
}
let (num_str, unit) = if let Some(pos) = rest.find(|c: char| !c.is_ascii_digit()) {
let num_part = &rest[..pos];
let unit_part = rest[pos..].trim_start();
if !matches!(unit_part.chars().next(), Some(c) if c.is_alphabetic()) {
return Err("Relative time must have a valid unit (h, m, d, etc.)".to_string());
}
(num_part, unit_part)
} else {
return Err("Relative time must have a unit (h, m, d, etc.)".to_string());
};
let num: i64 = num_str
.parse()
.map_err(|_| "Invalid number in relative time")?;
let signed_num = sign * num;
let seconds_factor: i64 = match unit {
"s" | "sec" | "secs" | "second" | "seconds" => 1,
"m" | "min" | "mins" | "minute" | "minutes" => 60,
"h" | "hour" | "hours" => 3_600,
"d" | "day" | "days" => 86_400,
"w" | "week" | "weeks" => 604_800,
_ => return Err(format!("Unknown time unit: {}", unit)),
};
let total_seconds = signed_num
.checked_mul(seconds_factor)
.ok_or_else(|| "Relative time is out of supported range".to_string())?;
chrono::Duration::try_seconds(total_seconds)
.ok_or_else(|| "Relative time is out of supported range".to_string())
}
fn parse_relative_time(arg: &str) -> Result<DateTime<Utc>, String> {
let duration = parse_duration(arg)?;
Utc::now()
.checked_add_signed(duration)
.ok_or_else(|| "Relative time is out of supported range".to_string())
}
fn parse_date_only(arg: &str) -> Option<DateTime<Utc>> {
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%Y-%m-%d") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%Y/%m/%d") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%m/%d/%Y") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%d.%m.%Y") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%B %d, %Y") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%d %B %Y") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
None
}
fn parse_time_only(arg: &str) -> Option<DateTime<Utc>> {
let today = Local::now().date_naive();
if let Ok(time) = chrono::NaiveTime::parse_from_str(arg, "%H:%M:%S") {
let naive_dt = today.and_time(time);
return Some(naive_dt.and_utc());
}
if let Ok(time) = chrono::NaiveTime::parse_from_str(arg, "%H:%M") {
let naive_dt = today.and_time(time);
return Some(naive_dt.and_utc());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
use proptest::prelude::*;
fn arb_utc_datetime() -> impl Strategy<Value = DateTime<Utc>> {
(-2208988800i64..=253402300799i64, 0u32..1_000_000_000).prop_map(|(secs, nanos)| {
chrono::Utc
.timestamp_opt(secs, nanos)
.single()
.expect("valid timestamp")
})
}
#[test]
fn test_adaptive_parser_basic() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2023-07-04T12:34:56Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
assert_eq!(dt.month(), 7);
assert_eq!(dt.day(), 4);
}
#[test]
fn test_format_reordering() {
let mut parser = AdaptiveTsParser::new();
let result1 = parser.parse_ts("2023-07-04 12:34:56");
assert!(result1.is_some());
let formats = parser.get_format_ordering();
assert!(!formats.is_empty());
assert_eq!(formats[0], "%Y-%m-%d %H:%M:%S%.f");
let result2 = parser.parse_ts("2023-07-05 13:45:07");
assert!(result2.is_some());
}
#[test]
fn test_unix_timestamp_parsing() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1735566123");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
let result = parser.parse_ts("1735566123000");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
}
#[test]
fn test_unix_float_timestamp_parsing() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1735566123.456");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.nanosecond() / 1_000_000, 456);
let result = parser.parse_ts("1735566123.456789");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
assert!(dt.nanosecond() >= 456_000_000);
let result = parser.parse_ts("1735566123456.789");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
let result = parser.parse_ts("123456.789");
assert!(result.is_none());
}
#[test]
fn test_looks_like_unix_timestamp() {
assert!(looks_like_unix_timestamp("1735566123"));
assert!(looks_like_unix_timestamp("1735566123000"));
assert!(looks_like_unix_timestamp("1735566123.456"));
assert!(looks_like_unix_timestamp("1735566123.456789"));
assert!(!looks_like_unix_timestamp("1735566123.456.789"));
assert!(!looks_like_unix_timestamp("1735566123a"));
assert!(!looks_like_unix_timestamp("2024-01-01"));
assert!(!looks_like_unix_timestamp(""));
assert!(!looks_like_unix_timestamp("."));
assert!(!looks_like_unix_timestamp(".123"));
}
#[test]
fn test_timestamp_field_identification() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert("ts".to_string(), Dynamic::from("2023-07-04T12:34:56Z"));
fields.insert("message".to_string(), Dynamic::from("test message"));
let config = TsConfig::default();
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_some());
let (field_name, value) = result.unwrap();
assert_eq!(field_name, "ts");
assert_eq!(value, "2023-07-04T12:34:56Z");
}
#[test]
fn test_custom_timestamp_field() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert(
"custom_time".to_string(),
Dynamic::from("2023-07-04T12:34:56Z"),
);
fields.insert("ts".to_string(), Dynamic::from("other_timestamp"));
let config = TsConfig {
custom_field: Some("custom_time".to_string()),
custom_format: None,
default_timezone: None,
};
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_some());
let (field_name, value) = result.unwrap();
assert_eq!(field_name, "custom_time");
assert_eq!(value, "2023-07-04T12:34:56Z");
}
#[test]
fn test_custom_timestamp_field_missing_does_not_fallback() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert("ts".to_string(), Dynamic::from("2023-07-04T12:34:56Z"));
let config = TsConfig {
custom_field: Some("custom_time".to_string()),
custom_format: None,
default_timezone: None,
};
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_none());
}
#[test]
fn test_timestamp_field_numeric_integer() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert("ts".to_string(), Dynamic::from(1735566123_i64));
let config = TsConfig::default();
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_some());
let (field_name, value) = result.unwrap();
assert_eq!(field_name, "ts");
assert_eq!(value, "1735566123");
}
#[test]
fn test_timestamp_field_numeric_float() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert("ts".to_string(), Dynamic::from(1735566123.456_f64));
let config = TsConfig::default();
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_some());
let (field_name, value) = result.unwrap();
assert_eq!(field_name, "ts");
assert!(value.starts_with("1735566123.456"));
}
#[test]
fn test_numeric_timestamp_parses_correctly() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1735566123");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
let result = parser.parse_ts("1735566123.456000000");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
assert!(dt.nanosecond() >= 456_000_000);
}
#[test]
fn test_custom_timestamp_field_non_string_does_not_fallback() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert("custom_time".to_string(), Dynamic::from(42));
fields.insert("ts".to_string(), Dynamic::from("2023-07-04T12:34:56Z"));
let config = TsConfig {
custom_field: Some("custom_time".to_string()),
custom_format: None,
default_timezone: None,
};
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_none());
}
#[test]
fn test_ordering_reset() {
let mut parser = AdaptiveTsParser::new();
parser.parse_ts("2023-07-04 12:34:56");
let formats_after = parser.get_format_ordering();
assert_eq!(formats_after[0], "%Y-%m-%d %H:%M:%S%.f");
parser.reset_ordering();
let formats_reset = parser.get_format_ordering();
assert_eq!(formats_reset[0], "%Y-%m-%dT%H:%M:%S%.fZ");
}
#[test]
fn test_invalid_timestamp() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("not-a-timestamp");
assert!(result.is_none());
let result = parser.parse_ts("2023-13-45T25:70:70Z");
assert!(result.is_none());
}
#[test]
fn test_fractional_part_with_non_digits_does_not_panic() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("4050-01-01T0,:00:00Z");
assert!(result.is_none());
}
#[test]
fn test_multiple_format_reordering() {
let mut parser = AdaptiveTsParser::new();
parser.parse_ts("2023-07-04 12:34:56"); parser.parse_ts("2023-07-05T13:45:07Z"); parser.parse_ts("2023-07-06 14:56:08");
let formats = parser.get_format_ordering();
assert_eq!(formats[0], "%Y-%m-%d %H:%M:%S%.f");
}
#[test]
fn test_journalctl_compatible_formats() {
let mut parser = AdaptiveTsParser::new();
let now = parser.parse_ts("now");
assert!(now.is_some());
let today = parser.parse_ts("today");
assert!(today.is_some());
let yesterday = parser.parse_ts("yesterday");
assert!(yesterday.is_some());
let tomorrow = parser.parse_ts("tomorrow");
assert!(tomorrow.is_some());
let one_hour_ago = parser.parse_ts("-1h");
assert!(one_hour_ago.is_some());
let thirty_min_from_now = parser.parse_ts("+30m");
assert!(thirty_min_from_now.is_some());
let date_only = parser.parse_ts("2023-07-04");
assert!(date_only.is_some());
let time_only = parser.parse_ts("15:30:45");
assert!(time_only.is_some());
}
#[test]
fn test_parse_timestamp_arg_with_timezone() {
let result = parse_timestamp_arg_with_timezone("2023-07-04T12:34:56Z", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("now", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-1h", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("invalid-timestamp", None);
assert!(result.is_err());
let result = parse_timestamp_arg_with_timezone("[9/Feb/2017:10:34:12 -0700]", None);
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.year(), 2017);
assert_eq!(dt.month(), 2);
assert_eq!(dt.day(), 9);
}
#[test]
fn test_relative_time_parsing() {
let result = parse_timestamp_arg_with_timezone("-30m", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("+2h", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-1d", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-2w", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-", None);
assert!(result.is_err());
let result = parse_timestamp_arg_with_timezone("-1x", None);
assert!(result.is_err());
let result = parse_timestamp_arg_with_timezone("1h", None); assert!(result.is_ok());
}
#[test]
fn test_relative_time_out_of_range_returns_error() {
assert!(parse_relative_time("111111111111h").is_err());
}
#[test]
fn test_parse_duration() {
let duration = parse_duration("+30m").unwrap();
assert_eq!(duration.num_minutes(), 30);
let duration = parse_duration("+1h").unwrap();
assert_eq!(duration.num_hours(), 1);
let duration = parse_duration("-30m").unwrap();
assert_eq!(duration.num_minutes(), -30);
let duration = parse_duration("1h").unwrap();
assert_eq!(duration.num_hours(), -1);
}
#[test]
fn test_parse_anchored_timestamp_since_plus() {
let base = chrono::Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap();
let result = parse_anchored_timestamp("since+30m", Some(base), None, None).unwrap();
let expected = chrono::Utc
.with_ymd_and_hms(2024, 1, 15, 10, 30, 0)
.unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_parse_anchored_timestamp_since_minus() {
let base = chrono::Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap();
let result = parse_anchored_timestamp("since-1h", Some(base), None, None).unwrap();
let expected = chrono::Utc.with_ymd_and_hms(2024, 1, 15, 9, 0, 0).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_parse_anchored_timestamp_until_plus() {
let base = chrono::Utc.with_ymd_and_hms(2024, 1, 15, 11, 0, 0).unwrap();
let result = parse_anchored_timestamp("until+1h", None, Some(base), None).unwrap();
let expected = chrono::Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_parse_anchored_timestamp_until_minus() {
let base = chrono::Utc.with_ymd_and_hms(2024, 1, 15, 11, 0, 0).unwrap();
let result = parse_anchored_timestamp("until-30m", None, Some(base), None).unwrap();
let expected = chrono::Utc
.with_ymd_and_hms(2024, 1, 15, 10, 30, 0)
.unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_parse_anchored_timestamp_now_plus() {
let before = chrono::Utc::now();
let result = parse_anchored_timestamp("now+30m", None, None, None).unwrap();
let after = chrono::Utc::now();
let expected_min = before + chrono::Duration::minutes(30) - chrono::Duration::seconds(5);
let expected_max = after + chrono::Duration::minutes(30) + chrono::Duration::seconds(5);
assert!(result >= expected_min && result <= expected_max);
}
#[test]
fn test_parse_anchored_timestamp_now_minus() {
let before = chrono::Utc::now();
let result = parse_anchored_timestamp("now-1h", None, None, None).unwrap();
let after = chrono::Utc::now();
let expected_min = before - chrono::Duration::hours(1) - chrono::Duration::seconds(5);
let expected_max = after - chrono::Duration::hours(1) + chrono::Duration::seconds(5);
assert!(result >= expected_min && result <= expected_max);
}
#[test]
fn test_parse_anchored_timestamp_missing_anchor() {
let result = parse_anchored_timestamp("since+30m", None, None, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("'since' anchor requires --since"));
let result = parse_anchored_timestamp("until+30m", None, None, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("'until' anchor requires --until"));
}
#[test]
fn test_parse_anchored_timestamp_fallback_to_normal() {
let result = parse_anchored_timestamp("2024-01-15T10:00:00Z", None, None, None);
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 15);
}
#[test]
fn test_date_only_parsing() {
let result = parse_timestamp_arg_with_timezone("2023-07-04", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("07/04/2023", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("04.07.2023", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("2023-13-45", None);
assert!(result.is_err());
}
#[test]
fn test_time_only_parsing() {
let result = parse_timestamp_arg_with_timezone("15:30:45", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("09:15", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("25:70:80", None);
assert!(result.is_err());
}
#[test]
fn test_comma_separated_milliseconds() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts_with_config("2010-04-24 07:52:09,487", None, Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2010);
assert_eq!(dt.month(), 4);
assert_eq!(dt.day(), 24);
assert_eq!(dt.hour(), 7);
assert_eq!(dt.minute(), 52);
assert_eq!(dt.second(), 9);
assert_eq!(
dt.timestamp_nanos_opt().unwrap() % 1_000_000_000,
487_000_000
);
let result = parser.parse_ts_with_config("2010-04-24 07:52:09,12", None, Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(
dt.timestamp_nanos_opt().unwrap() % 1_000_000_000,
120_000_000
);
let result = parser.parse_ts_with_config("2010-04-24 07:52:09,5", None, Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(
dt.timestamp_nanos_opt().unwrap() % 1_000_000_000,
500_000_000
);
}
#[test]
fn test_unix_timestamp_parsing_enhanced() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1735566123"); assert!(result.is_some());
let result = parser.parse_ts("1735566123000"); assert!(result.is_some());
let result = parser.parse_ts("1735566123000000"); assert!(result.is_some());
let result = parser.parse_ts("1735566123000000000"); assert!(result.is_some());
let result = parser.parse_ts("123"); assert!(result.is_none());
let result = parser.parse_ts("12345678901234567890123"); assert!(result.is_none());
}
#[test]
fn test_bracketed_timestamp_parsing() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("[9/Feb/2017:10:34:12 -0700]");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2017);
assert_eq!(dt.month(), 2);
assert_eq!(dt.day(), 9);
assert_eq!(dt.hour(), 17); assert_eq!(dt.minute(), 34);
assert_eq!(dt.second(), 12);
let result = parser.parse_ts("9/Feb/2017:10:34:12 -0700");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2017);
assert_eq!(dt.month(), 2);
let result = parser.parse_ts("[2023-07-04T12:34:56Z]");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
let result = parser.parse_ts("[2023-07-04 12:34:56]");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
}
proptest! {
#[test]
fn prop_parse_rfc3339_roundtrip(dt in arb_utc_datetime()) {
let mut parser = AdaptiveTsParser::new();
let input = dt.to_rfc3339();
let parsed = parser
.parse_ts_with_config(&input, None, None)
.expect("parser should accept RFC3339 timestamp");
prop_assert_eq!(parsed, dt);
}
#[test]
fn prop_parse_ignores_surrounding_whitespace(dt in arb_utc_datetime(), prefix in "[ \t]{0,3}", suffix in "[ \t]{0,3}") {
let mut parser = AdaptiveTsParser::new();
let raw = format!("{}{}{}", prefix, dt.to_rfc3339(), suffix);
let parsed = parser
.parse_ts_with_config(&raw, None, None)
.expect("parser should ignore surrounding whitespace");
prop_assert_eq!(parsed, dt);
}
}
#[test]
fn test_timezone_named_zones() {
let mut parser = AdaptiveTsParser::new();
let result =
parser.parse_ts_with_config("2023-07-04 12:00:00", None, Some("America/New_York"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.hour(), 16);
let result =
parser.parse_ts_with_config("2023-07-04 12:00:00", None, Some("Europe/London"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.hour(), 11);
let result = parser.parse_ts_with_config("2023-07-04 12:00:00", None, Some("Asia/Tokyo"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.hour(), 3);
let result = parser.parse_ts_with_config("2023-07-04 12:00:00", None, Some("Invalid/Zone"));
assert!(result.is_some());
}
#[test]
fn test_timezone_utc_explicit() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts_with_config("2023-07-04 12:00:00", None, Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.hour(), 12);
assert_eq!(dt.minute(), 0);
}
#[test]
fn test_dst_transition_spring_forward() {
let mut parser = AdaptiveTsParser::new();
let result =
parser.parse_ts_with_config("2023-03-12 01:59:00", None, Some("America/New_York"));
assert!(result.is_some());
let _result =
parser.parse_ts_with_config("2023-03-12 02:30:00", None, Some("America/New_York"));
let result =
parser.parse_ts_with_config("2023-03-12 03:00:00", None, Some("America/New_York"));
assert!(result.is_some());
}
#[test]
fn test_dst_transition_fall_back() {
let mut parser = AdaptiveTsParser::new();
let result =
parser.parse_ts_with_config("2023-11-05 01:30:00", None, Some("America/New_York"));
assert!(result.is_some());
let result =
parser.parse_ts_with_config("2023-11-05 02:30:00", None, Some("America/New_York"));
assert!(result.is_some());
let result =
parser.parse_ts_with_config("2023-11-05 03:00:00", None, Some("America/New_York"));
assert!(result.is_some());
}
#[test]
fn test_edge_dates_epoch_boundaries() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1970-01-01T00:00:00Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.timestamp(), 0);
let result = parser.parse_ts("1970-01-01T00:00:01Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.timestamp(), 1);
let result = parser.parse_ts("1969-12-31T23:59:59Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.timestamp(), -1);
}
#[test]
fn test_edge_dates_year_boundaries() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2000-01-01T00:00:00Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2000);
let result = parser.parse_ts("9999-12-31T23:59:59Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 9999);
let result = parser.parse_ts("0001-01-01T00:00:00Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 1);
}
#[test]
fn test_leap_year_february_29() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2020-02-29T12:00:00Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.month(), 2);
assert_eq!(dt.day(), 29);
let result = parser.parse_ts("2000-02-29T12:00:00Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.month(), 2);
assert_eq!(dt.day(), 29);
let result = parser.parse_ts("2019-02-29T12:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("1900-02-29T12:00:00Z");
assert!(result.is_none());
}
#[test]
fn test_unix_timestamp_negative() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("-1");
assert!(result.is_none());
}
#[test]
fn test_unix_timestamp_overflow() {
let mut parser = AdaptiveTsParser::new();
let _result = parser.parse_ts("9223372036854775807");
let result = parser.parse_ts("99999999999999999999"); assert!(result.is_none()); }
#[test]
fn test_custom_format_with_timezone() {
let mut parser = AdaptiveTsParser::new();
let custom_format = "%d/%m/%Y %H:%M:%S";
let result =
parser.parse_ts_with_config("15/07/2023 14:30:45", Some(custom_format), Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.day(), 15);
assert_eq!(dt.month(), 7);
assert_eq!(dt.year(), 2023);
assert_eq!(dt.hour(), 14);
assert_eq!(dt.minute(), 30);
let result = parser.parse_ts_with_config(
"15/07/2023 14:30:45",
Some(custom_format),
Some("America/New_York"),
);
assert!(result.is_some());
}
#[test]
fn test_custom_format_invalid() {
let mut parser = AdaptiveTsParser::new();
let custom_format = "%invalid%format";
let result = parser.parse_ts_with_config("2023-07-15 14:30:45", Some(custom_format), None);
assert!(result.is_some());
let custom_format = "%Y-%m-%d";
let result =
parser.parse_ts_with_config("not-a-valid-timestamp", Some(custom_format), None);
assert!(result.is_none());
}
#[test]
fn test_syslog_year_rollover() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts_with_config("Dec 31 23:59:59", None, Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.month(), 12);
assert_eq!(dt.day(), 31);
let result = parser.parse_ts_with_config("Jan 1 00:00:01", None, Some("UTC"));
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
}
#[test]
fn test_invalid_month_values() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2023-00-15T12:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("2023-13-15T12:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("2023-99-15T12:00:00Z");
assert!(result.is_none());
}
#[test]
fn test_invalid_day_values() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2023-07-00T12:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("2023-07-32T12:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("2023-02-31T12:00:00Z");
assert!(result.is_none());
}
#[test]
fn test_invalid_time_values() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2023-07-15T24:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("2023-07-15T25:00:00Z");
assert!(result.is_none());
let result = parser.parse_ts("2023-07-15T12:60:00Z");
assert!(result.is_none());
let _result = parser.parse_ts("2023-07-15T12:00:60Z");
let result = parser.parse_ts("2023-07-15T12:00:61Z");
assert!(result.is_none());
}
#[test]
fn test_malformed_timestamps() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("");
assert!(result.is_none());
let result = parser.parse_ts(" ");
assert!(result.is_none());
let result = parser.parse_ts("2023-07");
assert!(result.is_none());
let result = parser.parse_ts("20230715120000");
assert!(result.is_none());
let _result = parser.parse_ts("2023-07-15 12:00:00Z");
let result = parser.parse_ts("not-a-timestamp-at-all");
assert!(result.is_none());
let result = parser.parse_ts("2023@07@15T12:00:00Z");
assert!(result.is_none());
}
#[test]
fn test_very_long_timestamp_strings() {
let mut parser = AdaptiveTsParser::new();
let mut long_string = "2023-07-15T12:00:00Z".to_string();
long_string.push_str(&"x".repeat(10000));
let result = parser.parse_ts(&long_string);
assert!(result.is_none());
let result = parser.parse_ts("prefix 2023-07-15T12:00:00Z suffix");
assert!(result.is_none());
}
#[test]
fn test_rfc2822_parsing() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("Tue, 1 Jul 2003 10:52:37 +0200");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2003);
assert_eq!(dt.month(), 7);
assert_eq!(dt.day(), 1);
let result = parser.parse_ts("Sat, 15 Jul 2023 14:30:00 -0700");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
assert_eq!(dt.month(), 7);
assert_eq!(dt.day(), 15);
}
#[test]
fn test_duration_parsing_edge_cases() {
let result = parse_duration("0s");
assert!(result.is_ok());
assert_eq!(result.unwrap().num_seconds(), 0);
let result = parse_duration("999999h");
assert!(result.is_ok());
let result = parse_duration("100");
assert!(result.is_err());
let result = parse_duration("100x");
assert!(result.is_err());
let result = parse_duration("10 hours");
assert!(result.is_ok());
assert_eq!(result.unwrap().num_hours(), -10);
let result = parse_duration("10 hours");
assert!(result.is_ok());
assert_eq!(result.unwrap().num_hours(), -10);
let result = parse_duration("-0h");
assert!(result.is_ok());
assert_eq!(result.unwrap().num_seconds(), 0);
}
#[test]
fn test_duration_overflow_protection() {
let result = parse_duration("999999999999999999999999999999h");
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(err_msg.contains("out of supported range") || err_msg.contains("Invalid number"));
}
#[test]
fn test_looks_like_relative_time_edge_cases() {
assert!(looks_like_relative_time("1h"));
assert!(looks_like_relative_time("30m"));
assert!(looks_like_relative_time("2d"));
assert!(looks_like_relative_time("1 hour"));
assert!(looks_like_relative_time("30 minutes"));
assert!(!looks_like_relative_time("h")); assert!(!looks_like_relative_time("abc")); assert!(!looks_like_relative_time("1")); assert!(!looks_like_relative_time("1x")); assert!(!looks_like_relative_time("")); assert!(!looks_like_relative_time(" ")); }
#[test]
fn test_date_only_various_formats() {
let result = parse_date_only("2023-07-15");
assert!(result.is_some());
let result = parse_date_only("2023/07/15");
assert!(result.is_some());
let result = parse_date_only("07/15/2023");
assert!(result.is_some());
let result = parse_date_only("15.07.2023");
assert!(result.is_some());
let result = parse_date_only("July 15, 2023");
assert!(result.is_some());
let result = parse_date_only("15 July 2023");
assert!(result.is_some());
let result = parse_date_only("2023-13-45");
assert!(result.is_none());
let result = parse_date_only("not-a-date");
assert!(result.is_none());
}
#[test]
fn test_time_only_various_formats() {
let result = parse_time_only("14:30:45");
assert!(result.is_some());
let result = parse_time_only("14:30");
assert!(result.is_some());
let result = parse_time_only("25:00:00");
assert!(result.is_none());
let result = parse_time_only("14:60:00");
assert!(result.is_none());
let result = parse_time_only("not-a-time");
assert!(result.is_none());
}
#[test]
fn test_fractional_seconds_various_precisions() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2023-07-15T12:00:00.123Z");
assert!(result.is_some());
let result = parser.parse_ts("2023-07-15T12:00:00.123456Z");
assert!(result.is_some());
let result = parser.parse_ts("2023-07-15T12:00:00.123456789Z");
assert!(result.is_some());
let result = parser.parse_ts("2023-07-15T12:00:00.1Z");
assert!(result.is_some());
let result = parser.parse_ts("2023-07-15T12:00:00.12Z");
assert!(result.is_some());
let result = parser.parse_ts("2023-07-15T12:00:00.123456789012Z");
assert!(result.is_some());
}
#[test]
fn test_microsecond_unix_timestamp() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1689422400000000"); assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
assert_eq!(dt.month(), 7);
assert_eq!(dt.day(), 15);
}
#[test]
fn test_nanosecond_unix_timestamp() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1689422400000000000"); assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
assert_eq!(dt.month(), 7);
assert_eq!(dt.day(), 15);
}
}