initial commit after removing private test data

This commit is contained in:
_ 2025-08-11 06:13:38 +00:00
commit 294d95e80b
7 changed files with 14557 additions and 0 deletions

242
src/main.rs Normal file
View file

@ -0,0 +1,242 @@
use anyhow::{Context as _, Result, anyhow, bail};
use chrono::{DateTime, TimeZone as _, Utc};
use clap::Parser as _;
use icalendar::Component as _;
use std::{
convert::TryInto as _,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
#[derive(clap::Parser)]
struct CliIcsDebug {
/// Timezone to use for output (e.g. "Antarctica/South_Pole")
///
/// <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
#[arg(long)]
tz: String,
ics_paths: Vec<PathBuf>,
}
#[derive(clap::Subcommand)]
enum Commands {
IcsDebug(CliIcsDebug),
}
#[derive(clap::Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
struct EventInstance {
dtstart: DateTime<chrono_tz::Tz>,
summary: String,
}
struct Parameters {
/// Events before this time will be ignored if they cause an error
ignore_before: DateTime<rrule::Tz>,
/// Events before this time will not be shown
output_start: DateTime<rrule::Tz>,
/// Events after this time will not be shown
output_stop: DateTime<rrule::Tz>,
tz: chrono_tz::Tz,
}
fn _recurring_event(
params: &Parameters,
ev: &icalendar::Event,
rrule: &icalendar::Property,
) -> Result<Vec<DateTime<chrono_tz::Tz>>> {
let dtstart = ev
.properties()
.get("DTSTART")
.context("Data error - Event has no DTSTART")?;
let dtstart_s: String = dtstart
.clone()
.try_into()
.context("Bug - Can't roundtrip DTSTART")?;
let rrule_s: String = rrule
.clone()
.try_into()
.context("Bug - Can't roundtrip RRULE")?;
let set_s = format!("{dtstart_s}{rrule_s}");
let rrule = rrule::RRuleSet::from_str(&set_s)
.with_context(|| format!("RRuleSet parse failed `{set_s}`"))?;
let recurrences = rrule
.after(params.output_start)
.before(params.output_stop)
.all(10)
.dates;
let mut instances = vec![];
for dtstart in recurrences {
let dtstart = dtstart.with_timezone(&params.tz);
instances.push(dtstart);
}
Ok(instances)
}
fn recurring_event(
params: &Parameters,
ev: &icalendar::Event,
rrule: &icalendar::Property,
) -> Result<Vec<DateTime<chrono_tz::Tz>>> {
let dtstart = ev
.get_start()
.context("Data error - Event has no DTSTART")?;
let dtstart = normalize_date_perhaps_time(dtstart, &params.tz)?;
let rr = rrule::RRule::from_str(rrule.value())
.with_context(|| format!("RRule parse failed `{}`", rrule.value()))?;
if let Some(until) = rr.get_until() {
if *until < params.output_start {
return Ok(vec![]);
}
}
let dtstart = dtstart.with_timezone(&rrule::Tz::Tz(params.tz));
let rr = rr.build(dtstart)?;
let recurrences = rr
.after(params.output_start)
.before(params.output_stop)
.all(10)
.dates;
let mut instances = vec![];
for dtstart in recurrences {
let dtstart = dtstart.with_timezone(&params.tz);
instances.push(dtstart);
}
Ok(instances)
}
fn normalize_date_perhaps_time(
x: icalendar::DatePerhapsTime,
tz: &chrono_tz::Tz,
) -> Result<DateTime<chrono_tz::Tz>> {
Ok(match x {
icalendar::DatePerhapsTime::DateTime(x) => x
.try_into_utc()
.context("Data error - Could not convert event datetime to UTC")?
.with_timezone(tz),
icalendar::DatePerhapsTime::Date(date) => {
let midnight = chrono::NaiveTime::default();
match tz.from_local_datetime(&date.and_time(midnight)) {
chrono::offset::MappedLocalTime::Single(x) => x,
_ => bail!(
"Datetime doesn't map to a single unambiguous datetime when converting to our timezone"
),
}
}
})
}
fn event_instances(params: &Parameters, ev: &icalendar::Event) -> Result<Vec<EventInstance>> {
let instances = if let Some(rrule) = ev.properties().get("RRULE") {
recurring_event(params, ev, rrule)?
} else {
// Event that occurs once
let dtstart = ev.get_start().context("Data error - Event has no start")?;
let dtstart = normalize_date_perhaps_time(dtstart, &params.tz)?;
if dtstart < params.output_start || dtstart > params.output_stop {
return Ok(vec![]);
}
vec![dtstart]
};
let summary = ev
.get_summary()
.unwrap_or("Data error BXH45NAR - No summary in event");
let instances = instances
.into_iter()
.map(|dtstart| EventInstance {
dtstart,
summary: summary.to_string(),
})
.collect();
Ok(instances)
}
fn read_ics(params: &Parameters, path: &Path) -> Result<Vec<EventInstance>> {
let mut instances = vec![];
let s = std::fs::read_to_string(path)?;
let cal: icalendar::Calendar = s.parse().map_err(|s| anyhow!("parse error {s}"))?;
for component in &cal.components {
let icalendar::CalendarComponent::Event(ev) = component else {
continue;
};
let eis = match event_instances(params, ev)
.with_context(|| format!("Failed to process event with UID '{:?}'", ev.get_uid()))
{
Ok(x) => x,
Err(e) => {
if ev.get_last_modified().context("Event has no timestamp")? < params.ignore_before
{
eprintln!("Ignoring error from very old event {e:?}");
continue;
} else {
Err(e)?
}
}
};
for ei in eis {
instances.push(ei);
}
}
Ok(instances)
}
fn main_ics_debug(cli: CliIcsDebug) -> Result<()> {
let tz = cli.tz.parse().context("Couldn't parse timezone name")?;
let now = Utc::now().with_timezone(&rrule::Tz::Tz(chrono_tz::UTC));
let params = Parameters {
ignore_before: now - Duration::from_secs(86_400 * 365 * 2),
output_start: now - Duration::from_secs(86_400 * 15),
output_stop: now + Duration::from_secs(86_400 * 45),
tz,
};
let mut instances = vec![];
for path in cli.ics_paths {
let eis =
read_ics(&params, &path).with_context(|| format!("Failed to parse file `{path:?}`"))?;
for ei in eis {
instances.push(ei);
}
}
instances.sort_by_key(|ei| ei.dtstart);
for instance in instances {
let EventInstance { dtstart, summary } = instance;
println!("{dtstart} - {summary}");
}
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::try_parse()?;
match cli.command {
Commands::IcsDebug(x) => main_ics_debug(x),
}
}