From cad8652179488e523c544d200d680851baa4888b Mon Sep 17 00:00:00 2001 From: core Date: Mon, 23 Mar 2026 12:11:04 -0400 Subject: [PATCH] feat: fix edgecases --- Cargo.lock | 72 +++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 108 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 158 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38790e957afee01215da01231f7cc0b53de003d0..05397a535d10785d5892cbc533b8a8e7bce08c3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -42,6 +51,7 @@ dependencies = [ "chrono", "chrono-tz", "icalendar", + "rrule", "walkdir", ] @@ -331,6 +341,48 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rrule" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74" +dependencies = [ + "chrono", + "chrono-tz", + "log", + "regex", + "thiserror", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -417,6 +469,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 4329c8e19b1e10bfc50c1e13e1ea5edb27461c58..0f9763f8caed344b6a593d5e113a04cfcec84c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ walkdir = "2.5" icalendar = "0.16" chrono = "0.4" chrono-tz = "0.10" +rrule = "0.14" diff --git a/src/main.rs b/src/main.rs index 57608ed89653cf70693f771a1bb0313af6f55ad1..e0c03dc452ae90f124045ceb1f748d3302e0fc28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use std::env::args; use std::str::FromStr; - use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc}; use chrono_tz::Tz; use icalendar::{Calendar, CalendarComponent, CalendarDateTime, Component, DatePerhapsTime, EventLike}; +use rrule::RRuleSet; use walkdir::WalkDir; fn to_utc(dpt: DatePerhapsTime) -> Option> { @@ -31,6 +31,21 @@ fn to_utc(dpt: DatePerhapsTime) -> Option> { } } +fn dpt_to_dtstart_string(dpt: &DatePerhapsTime) -> String { + match dpt { + DatePerhapsTime::DateTime(cdt) => match cdt { + CalendarDateTime::Utc(dt) => format!("DTSTART:{}", dt.format("%Y%m%dT%H%M%SZ")), + CalendarDateTime::Floating(naive) => { + format!("DTSTART:{}", naive.format("%Y%m%dT%H%M%S")) + } + CalendarDateTime::WithTimezone { date_time, tzid } => { + format!("DTSTART;TZID={}:{}", tzid, date_time.format("%Y%m%dT%H%M%S")) + } + }, + DatePerhapsTime::Date(date) => format!("DTSTART;VALUE=DATE:{}", date.format("%Y%m%d")), + } +} + fn is_imminent(time_delta: TimeDelta) -> bool { time_delta <= TimeDelta::hours(2) } @@ -104,19 +119,47 @@ fn main() { let Some(end) = event.get_end() else { continue; }; + let dtstart_str = dpt_to_dtstart_string(&start); let Some(dt) = to_utc(start) else { continue }; let Some(dte) = to_utc(end) else { continue }; - if dte > now { - let summary = event.get_summary().unwrap_or("(no title)").to_string(); - let location = event.get_location().map(|u| u.to_string()); - upcoming.push((dt, dte, summary, if with_loc { location } else { None })); + + let summary = event.get_summary().unwrap_or("(no title)").to_string(); + let location = event.get_location().map(|u| u.to_string()); + let loc = if with_loc { location } else { None }; + + if let Some(rrule_str) = event.property_value("RRULE") { + let mut rrule_input = format!("{}\nRRULE:{}", dtstart_str, rrule_str); + if let Some(exdate_prop) = event.properties().get("EXDATE") { + rrule_input.push_str("\nEXDATE"); + for (key, param) in exdate_prop.params() { + rrule_input.push_str(&format!(";{}={}", key, param.value())); + } + rrule_input.push(':'); + rrule_input.push_str(exdate_prop.value()); + } + if let Ok(rrule_set) = rrule_input.parse::() { + let duration = dte - dt; + let search_from = (now - duration).with_timezone(&rrule::Tz::UTC); + let result = rrule_set.after(search_from).all(5); + for occ in result.dates { + let occ_utc: DateTime = occ.with_timezone(&Utc); + let occ_end = occ_utc + duration; + if occ_end > now { + upcoming.push((occ_utc, occ_end, summary.clone(), loc.clone())); + } + } + } + } else { + if dte > now { + upcoming.push((dt, dte, summary, loc)); + } } } } } upcoming.sort_by_key(|(dt, _, _, _)| *dt); - + // write the message // "nothing left today" if next event is past midnight or there are no events if upcoming.is_empty() { @@ -147,33 +190,52 @@ fn main() { if time_until_next < TimeDelta::zero() { let time_until_end = next.1 - now; let end_local = next.1.with_timezone(&Local); - // is end imminent - if is_ending_imminent(time_until_end) { - let followup = upcoming.get(1); - if let Some((following_start, _, following, following_loc)) = followup { - print!("{}", show_imminent_ending(time_until_end, &next.2, next.3.clone())); - let turnaround_time = *following_start - next.1; + let second = upcoming.get(1); + + let overlap = second.filter(|(b_start, _, _, _)| *b_start < next.1); + + let a_fmt = if is_ending_imminent(time_until_end) { + show_imminent_ending(time_until_end, &next.2, next.3.clone()) + } else { + show_distant_ending(end_local, &next.2, next.3.clone()) + }; + + if let Some((b_start, b_end, b_name, b_loc)) = overlap { + let time_until_b = *b_start - now; + if *b_start < now { + let time_until_b_end = *b_end - now; + let b_fmt = if is_ending_imminent(time_until_b_end) { + show_imminent_ending(time_until_b_end, b_name, b_loc.clone()) + } else { + show_distant_ending(b_end.with_timezone(&Local), b_name, b_loc.clone()) + }; + println!("{}, and {}", a_fmt, b_fmt); + } else if is_imminent(time_until_b) { + println!("{}, with {}", a_fmt, show_imminent(time_until_b, b_name, b_loc.clone())); + } else { + println!("{}", a_fmt); + } + return; + } + + if is_ending_imminent(time_until_end) { + if let Some((b_start, _, b_name, b_loc)) = second { + let turnaround_time = *b_start - next.1; if !is_soon(turnaround_time) { - println!(); + println!("{}", a_fmt); return; } - if is_imminent(turnaround_time) { - println!(", then {}", show_imminent(turnaround_time, following, following_loc.clone())); - return; + println!("{}, then {}", a_fmt, show_imminent(turnaround_time, b_name, b_loc.clone())); } else { - println!(", then {}", show_distant_event(following_start.with_timezone(&Local), following)); - return; + println!("{}, then {}", a_fmt, show_distant_event(b_start.with_timezone(&Local), b_name)); } - } else { - println!("{}", show_imminent_ending(time_until_end, &next.2, next.3.clone())); return; } - } else { - println!("{}", show_distant_ending(end_local, &next.2, next.3.clone())); - return; } + println!("{}", a_fmt); + return; } // next event is today or imminent (or both)