caltemps 0.3.1

A tool to query and report on your iCalendar data from vDirs.
Documentation
use chrono::{DateTime, Datelike, Local, TimeZone};
use icalendar::{Component, DatePerhapsTime};
use regex::Regex;
use std::cmp::{max, min};

use super::date_range::CalTempsDateRange;

#[derive(Debug, Clone)]
pub struct CalTempsEntry {
    pub is_event: bool,
    pub summary: String,
    pub description: String,
    pub start: Option<DateTime<Local>>,
    pub end: Option<DateTime<Local>>,
}

impl CalTempsEntry {
    pub fn duration_minutes(&self) -> Option<i64> {
        match self.start {
            Some(ds) => self.end.map(|de| (de - ds).num_minutes()),
            _ => None,
        }
    }
    pub fn date_range_intersection_minutes(&self, date_range: &CalTempsDateRange) -> Option<i64> {
        match self.start {
            Some(ds) => match self.end {
                Some(de) => {
                    let mins = (match date_range.end {
                        Some(dre) => min(de, dre),
                        _ => de,
                    } - match date_range.start {
                        Some(drs) => max(ds, drs),
                        _ => ds,
                    })
                    .num_minutes();
                    if mins < 0 { None } else { Some(mins) }
                }
                _ => None,
            },
            _ => None,
        }
    }
    fn get_matches(&self, pattern: &str) -> Vec<&str> {
        let re = Regex::new(pattern).unwrap();
        let mut v: Vec<_> = re.find_iter(&self.summary).map(|m| m.as_str()).collect();
        v.sort_by_cached_key(|t| t.to_lowercase());
        v
    }
    pub fn full_tags(&self) -> Vec<&str> {
        self.hash_tags()
            .into_iter()
            .chain(self.who_tags())
            .collect()
    }
    pub fn at_tags(&self) -> Vec<&str> {
        self.get_matches(r"([@][\w]+)")
    }
    pub fn hash_tags(&self) -> Vec<&str> {
        self.get_matches(r"([#][\w]+)")
    }
    pub fn plus_tags(&self) -> Vec<&str> {
        self.get_matches(r"([+][\w]+)")
    }
    pub fn minus_tags(&self) -> Vec<&str> {
        self.get_matches(r"([-][\w]+)")
    }
    pub fn star_tags(&self) -> Vec<&str> {
        self.get_matches(r"([*][\w]+)")
    }
    pub fn who_tags(&self) -> Vec<&str> {
        self.get_matches(r"([!][\w]+)")
    }
}

fn dpt_to_dt(dpt: Option<DatePerhapsTime>) -> Option<DateTime<Local>> {
    match dpt {
        Some(icalendar::DatePerhapsTime::DateTime(cdt)) => cdt.try_into_utc().map(|d| d.into()),
        Some(icalendar::DatePerhapsTime::Date(nd)) => Some(
            Local
                .with_ymd_and_hms(nd.year(), nd.month(), nd.day(), 0, 0, 0)
                .unwrap(),
        ),
        _ => None,
    }
}

fn get_matching_entry_basic(
    active_filter: String,
    date_range: &CalTempsDateRange,
    summary: Option<&str>,
    description: Option<&str>,
    start: Option<DateTime<Local>>,
    end: Option<DateTime<Local>>,
) -> Option<CalTempsEntry> {
    if let Some(summary_str) = summary {
        if summary_str.contains(&active_filter) {
            return match start {
                Some(dts) => {
                    if date_range.start.unwrap_or(dts) <= dts {
                        match end {
                            Some(dte) => {
                                if dte <= date_range.end.unwrap_or(dte) {
                                    Some(CalTempsEntry {
                                        summary: summary_str.into(),
                                        description: description.unwrap_or_default().into(),
                                        start,
                                        end,
                                        is_event: true,
                                    })
                                } else {
                                    // Ignore those not matching end
                                    None
                                }
                            }
                            // Ignore those without end data(!)
                            _ => None,
                        }
                    } else {
                        // Ignore not matching start data
                        None
                    }
                }
                // Ignoring those without start data
                _ => None,
            };
        }
    }
    // Ignoring invalid or non-matching summaries
    None
}

pub fn get_matching_entry(
    component: icalendar::CalendarComponent,
    active_filter: String,
    date_range: &CalTempsDateRange,
) -> Option<CalTempsEntry> {
    match component {
        icalendar::CalendarComponent::Event(item) => get_matching_entry_basic(
            active_filter,
            date_range,
            item.get_summary(),
            item.get_description(),
            dpt_to_dt(item.get_start()),
            dpt_to_dt(item.get_end()),
        ),
        // TODO: we may want to implement support for other components
        _ => None,
    }
}