initial commit after removing private test data
This commit is contained in:
commit
294d95e80b
7 changed files with 14557 additions and 0 deletions
242
src/main.rs
Normal file
242
src/main.rs
Normal 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(¶ms.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, ¶ms.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(¶ms.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, ¶ms.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(¶ms, &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),
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue