~core/calstr

cad8652179488e523c544d200d680851baa4888b — core 3 months ago a49a42f
feat: fix edgecases
3 files changed, 158 insertions(+), 23 deletions(-)

M Cargo.lock
M Cargo.toml
M src/main.rs
M Cargo.lock => Cargo.lock +72 -0
@@ 3,6 3,15 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 42,6 51,7 @@ dependencies = [
 "chrono",
 "chrono-tz",
 "icalendar",
 "rrule",
 "walkdir",
]



@@ 332,6 342,48 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 418,6 470,26 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +1 -0
@@ 8,3 8,4 @@ walkdir = "2.5"
icalendar = "0.16"
chrono = "0.4"
chrono-tz = "0.10"
rrule = "0.14"

M src/main.rs => src/main.rs +85 -23
@@ 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<DateTime<Utc>> {


@@ 31,6 31,21 @@ fn to_utc(dpt: DatePerhapsTime) -> Option<DateTime<Utc>> {
    }
}

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::<RRuleSet>() {
                        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<Utc> = 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)