use crate::model::Recollection;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatedContext {
pub timeline: String,
pub now: Option<String>,
}
#[must_use]
pub fn format_dated_context(facts: &[Recollection], date_field: &str) -> DatedContext {
let mut dated: Vec<(i64, String, &str)> = Vec::new();
let mut undated: Vec<&str> = Vec::new();
for fact in facts {
match fact_date(fact, date_field) {
Some((key, formatted)) => dated.push((key, formatted, &fact.content)),
None => undated.push(&fact.content),
}
}
dated.sort_by_key(|(key, _, _)| *key);
let now = dated.last().map(|(_, formatted, _)| formatted.clone());
let lines = dated
.iter()
.map(|(_, date, content)| format!("- [{date}] {content}"))
.chain(undated.iter().map(|content| format!("- {content}")))
.collect::<Vec<_>>()
.join("\n");
DatedContext {
timeline: lines,
now,
}
}
fn fact_date(fact: &Recollection, date_field: &str) -> Option<(i64, String)> {
let raw = fact.metadata.as_ref()?.get(date_field)?.as_i64()?;
Some((raw, fmt_date(raw)?))
}
fn fmt_date(ts: i64) -> Option<String> {
let (year, month, day) = decompose_ymd(ts)?;
Some(format!("{year:04}-{month:02}-{day:02}"))
}
fn decompose_ymd(ts: i64) -> Option<(i64, i64, i64)> {
if ts <= 0 {
return None;
}
let (year, month, day) = (ts / 10_000, (ts / 100) % 100, ts % 100);
if !(1..=12).contains(&month) {
return None;
}
(1..=days_in_month(year, month))
.contains(&day)
.then_some((year, month, day))
}
fn days_in_month(year: i64, month: i64) -> i64 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
fn is_leap_year(year: i64) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
#[cfg(test)]
#[path = "dated_context_tests.rs"]
mod tests;