use chrono::{DateTime, NaiveDate, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CognifyTimestamp {
pub year: u16,
pub month: u8, pub day: u8, pub hour: u8, pub minute: u8, pub second: u8, pub time_at: i64,
pub timestamp_str: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CognifyInterval {
pub time_from: CognifyTimestamp,
pub time_to: CognifyTimestamp,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TemporalEvent {
pub name: String,
pub description: Option<String>,
pub location: Option<String>,
pub at: Option<CognifyTimestamp>,
pub during: Option<CognifyInterval>,
#[serde(default)]
pub attributes: Vec<EventAttribute>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct EventAttribute {
pub entity: String,
pub entity_type: String,
pub relationship: String,
}
fn default_month() -> u8 {
1
}
fn default_day() -> u8 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RawExtractedTimestamp {
pub year: u16,
#[serde(default = "default_month")]
pub month: u8, #[serde(default = "default_day")]
pub day: u8, #[serde(default)]
pub hour: u8, #[serde(default)]
pub minute: u8, #[serde(default)]
pub second: u8, }
pub fn to_cognify_timestamp(raw: RawExtractedTimestamp) -> Option<CognifyTimestamp> {
let naive = NaiveDate::from_ymd_opt(raw.year as i32, raw.month as u32, raw.day as u32)?
.and_hms_opt(raw.hour as u32, raw.minute as u32, raw.second as u32)?;
let time_at = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc).timestamp_millis(); let timestamp_str = format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
raw.year, raw.month, raw.day, raw.hour, raw.minute, raw.second
);
Some(CognifyTimestamp {
year: raw.year,
month: raw.month,
day: raw.day,
hour: raw.hour,
minute: raw.minute,
second: raw.second,
time_at,
timestamp_str,
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code — panics are acceptable failures"
)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
#[test]
fn to_cognify_timestamp_happy_path() {
let raw = RawExtractedTimestamp {
year: 2024,
month: 3,
day: 15,
hour: 0,
minute: 0,
second: 0,
};
let result = to_cognify_timestamp(raw).unwrap();
let expected_dt = Utc.with_ymd_and_hms(2024, 3, 15, 0, 0, 0).unwrap();
assert_eq!(result.time_at, expected_dt.timestamp_millis());
assert_eq!(result.timestamp_str, "2024-03-15 00:00:00");
assert_eq!(result.year, 2024);
assert_eq!(result.month, 3);
assert_eq!(result.day, 15);
}
#[test]
fn to_cognify_timestamp_with_time_components() {
let raw = RawExtractedTimestamp {
year: 2024,
month: 7,
day: 4,
hour: 14,
minute: 30,
second: 45,
};
let result = to_cognify_timestamp(raw).unwrap();
let expected_dt = Utc.with_ymd_and_hms(2024, 7, 4, 14, 30, 45).unwrap();
assert_eq!(result.time_at, expected_dt.timestamp_millis());
assert_eq!(result.timestamp_str, "2024-07-04 14:30:45");
assert_eq!(result.hour, 14);
assert_eq!(result.minute, 30);
assert_eq!(result.second, 45);
}
#[test]
fn to_cognify_timestamp_invalid_dates_return_none() {
let raw = RawExtractedTimestamp {
year: 2024,
month: 13,
day: 1,
hour: 0,
minute: 0,
second: 0,
};
assert!(to_cognify_timestamp(raw).is_none());
let raw = RawExtractedTimestamp {
year: 2024,
month: 2,
day: 30,
hour: 0,
minute: 0,
second: 0,
};
assert!(to_cognify_timestamp(raw).is_none());
}
#[test]
fn to_cognify_timestamp_serde_defaults() {
let raw = RawExtractedTimestamp {
year: 1889,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
};
let result = to_cognify_timestamp(raw).unwrap();
let expected_dt = Utc.with_ymd_and_hms(1889, 1, 1, 0, 0, 0).unwrap();
assert_eq!(result.time_at, expected_dt.timestamp_millis());
assert_eq!(result.timestamp_str, "1889-01-01 00:00:00");
assert_eq!(result.year, 1889);
assert_eq!(result.month, 1);
assert_eq!(result.day, 1);
}
}