From f89135fce588414c8f71af83edc453bee36183b9 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Tue, 20 Jan 2026 11:12:11 -0800 Subject: [PATCH] server: extract time range in addition to date from more calendar types --- server/src/email_extract.rs | 93 ++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/server/src/email_extract.rs b/server/src/email_extract.rs index 19d68a1..af9ca05 100644 --- a/server/src/email_extract.rs +++ b/server/src/email_extract.rs @@ -178,11 +178,36 @@ pub fn extract_calendar_metadata_from_mail( let month = &caps[2]; let day = &caps[3]; let year = &caps[4]; + let start_hour: u32 = caps[5].parse().unwrap_or(0); + let start_min: u32 = caps.get(6).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0); + let start_ampm = &caps[7]; + let end_hour: u32 = caps[8].parse().unwrap_or(0); + let end_min: u32 = caps.get(9).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0); + let end_ampm = &caps[10]; + + // Convert 12-hour to 24-hour format + let start_hour_24 = if start_ampm == "pm" && start_hour != 12 { + start_hour + 12 + } else if start_ampm == "am" && start_hour == 12 { + 0 + } else { + start_hour + }; + let end_hour_24 = if end_ampm == "pm" && end_hour != 12 { + end_hour + 12 + } else if end_ampm == "am" && end_hour == 12 { + 0 + } else { + end_hour + }; + let date_str = format!("{} {} {}", month, day, year); if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%b %d %Y") { - let ymd = date.format("%Y%m%d").to_string(); - start_date = Some(ymd.clone()); - end_date = Some(ymd); + // Store date with time in YYYYMMDDTHHMMSS format for start/end + let start_dt = format!("{}T{:02}{:02}00", date.format("%Y%m%d"), start_hour_24, start_min); + let end_dt = format!("{}T{:02}{:02}00", date.format("%Y%m%d"), end_hour_24, end_min); + start_date = Some(start_dt); + end_date = Some(end_dt); } } else { // Pattern: from Thu Sep 11 to Fri Jan 30, 2026 @@ -375,13 +400,37 @@ pub fn extract_calendar_metadata_from_mail( let (local_fmt_start, local_fmt_end) = if let (Some(ref start_str), Some(ref end_str)) = (&start_date, &end_date) { - // Parse YYYYMMDD format dates - let start_d = NaiveDate::parse_from_str(start_str, "%Y%m%d").ok(); - let end_d = NaiveDate::parse_from_str(end_str, "%Y%m%d").ok(); + // Parse dates - try YYYYMMDDTHHMMSS format first (with time), then YYYYMMDD (date only) + let (start_d, start_time) = if start_str.contains('T') { + let parts: Vec<&str> = start_str.split('T').collect(); + let date = NaiveDate::parse_from_str(parts[0], "%Y%m%d").ok(); + let time = if parts.len() > 1 { + chrono::NaiveTime::parse_from_str(parts[1], "%H%M%S").ok() + } else { + None + }; + (date, time) + } else { + (NaiveDate::parse_from_str(start_str, "%Y%m%d").ok(), None) + }; + + let (end_d, end_time) = if end_str.contains('T') { + let parts: Vec<&str> = end_str.split('T').collect(); + let date = NaiveDate::parse_from_str(parts[0], "%Y%m%d").ok(); + let time = if parts.len() > 1 { + chrono::NaiveTime::parse_from_str(parts[1], "%H%M%S").ok() + } else { + None + }; + (date, time) + } else { + (NaiveDate::parse_from_str(end_str, "%Y%m%d").ok(), None) + }; if let (Some(start), Some(end)) = (start_d, end_d) { - // For all-day events, end date is exclusive, so we need to subtract one day - let display_end = if end > start { + // For all-day events (no time), end date is exclusive, so we need to subtract one day + let is_allday = start_time.is_none() && end_time.is_none(); + let display_end = if is_allday && end > start { end.pred_opt().unwrap_or(end) } else { end @@ -398,9 +447,17 @@ pub fn extract_calendar_metadata_from_mail( } } - // Format dates for display - let fmt_start = start.format("%a %b %e, %Y").to_string(); - let fmt_end = display_end.format("%a %b %e, %Y").to_string(); + // Format dates for display - include time if available + let fmt_start = if let Some(t) = start_time { + format!("{} {}", start.format("%a %b %e, %Y"), t.format("%-I:%M %p")) + } else { + start.format("%a %b %e, %Y").to_string() + }; + let fmt_end = if let Some(t) = end_time { + format!("{} {}", display_end.format("%a %b %e, %Y"), t.format("%-I:%M %p")) + } else { + display_end.format("%a %b %e, %Y").to_string() + }; (fmt_start, fmt_end) } else { (start_val.clone(), end_val.clone()) @@ -2395,9 +2452,9 @@ mod tests { assert_eq!(meta.summary, Some("Dentist appt".to_string())); // Organizer: from From header, extract email address assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string())); - // Dates: should extract Sep 23, 2025, 3pm-4pm - assert_eq!(meta.start_date, Some("20250923".to_string())); - assert_eq!(meta.end_date, Some("20250923".to_string())); + // Dates: should extract Sep 23, 2025, 3pm-4pm (15:00-16:00) + assert_eq!(meta.start_date, Some("20250923T150000".to_string())); + assert_eq!(meta.end_date, Some("20250923T160000".to_string())); // Should not be recurring if let Some(ref html) = meta.body_html { assert!( @@ -2625,13 +2682,17 @@ mod tests { assert_eq!(meta.summary, Some("painting class".to_string())); assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string())); // Dates: Thursday Feb 12, 2026 7pm - 9pm (same day event with time) - assert_eq!(meta.start_date, Some("20260212".to_string())); - assert_eq!(meta.end_date, Some("20260212".to_string())); + // Start: 7pm = 19:00, End: 9pm = 21:00 + assert_eq!(meta.start_date, Some("20260212T190000".to_string())); + assert_eq!(meta.end_date, Some("20260212T210000".to_string())); // Assert ical summary is rendered and shows Feb 12 highlighted let html = meta.body_html.expect("body_html"); println!("Rendered HTML: {}", html); assert!(html.contains("ical-flex"), "Calendar widget should be rendered"); assert!(html.contains(r#"data-event-day="2026-02-12""#), "Feb 12 should be highlighted"); + // Verify time is displayed in the HTML + assert!(html.contains("7:00 PM") || html.contains("7pm") || html.contains("19:00"), + "HTML should contain the start time"); } #[test]