use chrono::{Datelike, Local, NaiveDate};
use todoist_api_rs::sync::{Collaborator, Item, Label, Project, Section};
use super::ast::{AssignedTarget, Filter};
#[derive(Debug, Clone)]
pub struct FilterContext<'a> {
projects: &'a [Project],
sections: &'a [Section],
labels: &'a [Label],
collaborators: &'a [Collaborator],
current_user_id: Option<&'a str>,
}
impl<'a> FilterContext<'a> {
pub fn new(projects: &'a [Project], sections: &'a [Section], labels: &'a [Label]) -> Self {
Self {
projects,
sections,
labels,
collaborators: &[],
current_user_id: None,
}
}
pub fn with_assignment_context(
mut self,
collaborators: &'a [Collaborator],
current_user_id: Option<&'a str>,
) -> Self {
self.collaborators = collaborators;
self.current_user_id = current_user_id;
self
}
fn find_collaborator_by_name(&self, name: &str) -> Option<&Collaborator> {
let name_lower = name.to_lowercase();
self.collaborators.iter().find(|c| {
c.full_name
.as_ref()
.is_some_and(|n| n.to_lowercase().contains(&name_lower))
|| c.email
.as_ref()
.is_some_and(|e| e.to_lowercase().contains(&name_lower))
})
}
pub fn find_project_by_name(&self, name: &str) -> Option<&Project> {
let name_lower = name.to_lowercase();
self.projects
.iter()
.find(|p| !p.is_deleted && p.name.to_lowercase() == name_lower)
}
pub fn get_project_ids_with_subprojects(&self, name: &str) -> Vec<&str> {
let Some(root_project) = self.find_project_by_name(name) else {
return vec![];
};
let mut ids = vec![root_project.id.as_str()];
self.collect_subproject_ids(&root_project.id, &mut ids);
ids
}
fn collect_subproject_ids<'b>(&'b self, parent_id: &str, ids: &mut Vec<&'b str>) {
for project in self.projects.iter() {
if project.parent_id.as_deref() == Some(parent_id) && !project.is_deleted {
ids.push(&project.id);
self.collect_subproject_ids(&project.id, ids);
}
}
}
pub fn find_section_by_name(&self, name: &str) -> Option<&Section> {
let name_lower = name.to_lowercase();
self.sections
.iter()
.find(|s| !s.is_deleted && s.name.to_lowercase() == name_lower)
}
pub fn label_exists(&self, name: &str) -> bool {
let name_lower = name.to_lowercase();
self.labels
.iter()
.any(|l| !l.is_deleted && l.name.to_lowercase() == name_lower)
}
}
#[derive(Debug)]
pub struct FilterEvaluator<'a> {
filter: &'a Filter,
context: &'a FilterContext<'a>,
}
impl<'a> FilterEvaluator<'a> {
pub fn new(filter: &'a Filter, context: &'a FilterContext<'a>) -> Self {
Self { filter, context }
}
pub fn matches(&self, item: &Item) -> bool {
self.evaluate_filter(self.filter, item)
}
pub fn filter_items<'b>(&self, items: &'b [Item]) -> Vec<&'b Item> {
let estimated_capacity = (items.len() / 10).max(16);
let mut result = Vec::with_capacity(estimated_capacity);
for item in items {
if self.matches(item) {
result.push(item);
}
}
result
}
fn evaluate_filter(&self, filter: &Filter, item: &Item) -> bool {
match filter {
Filter::Today => self.is_due_today(item),
Filter::Tomorrow => self.is_due_tomorrow(item),
Filter::Overdue => self.is_overdue(item),
Filter::NoDate => self.has_no_date(item),
Filter::Next7Days => self.is_due_within_7_days(item),
Filter::SpecificDate { month, day } => self.is_due_on_specific_date(item, *month, *day),
Filter::Priority1 => item.priority == 4,
Filter::Priority2 => item.priority == 3,
Filter::Priority3 => item.priority == 2,
Filter::Priority4 => item.priority == 1,
Filter::Label(name) => self.has_label(item, name),
Filter::NoLabels => self.has_no_labels(item),
Filter::Project(name) => self.in_project(item, name),
Filter::ProjectWithSubprojects(name) => self.in_project_or_subproject(item, name),
Filter::Section(name) => self.in_section(item, name),
Filter::AssignedTo(target) => self.is_assigned_to(item, target),
Filter::AssignedBy(target) => self.is_assigned_by(item, target),
Filter::Assigned => item.responsible_uid.is_some(),
Filter::NoAssignee => item.responsible_uid.is_none(),
Filter::And(left, right) => {
self.evaluate_filter(left, item) && self.evaluate_filter(right, item)
}
Filter::Or(left, right) => {
self.evaluate_filter(left, item) || self.evaluate_filter(right, item)
}
Filter::Not(inner) => !self.evaluate_filter(inner, item),
}
}
fn is_due_today(&self, item: &Item) -> bool {
let Some(due) = &item.due else {
return false;
};
let today = Local::now().date_naive();
self.parse_due_date(&due.date)
.is_some_and(|due_date| due_date == today)
}
fn is_due_tomorrow(&self, item: &Item) -> bool {
let Some(due) = &item.due else {
return false;
};
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
self.parse_due_date(&due.date)
.is_some_and(|due_date| due_date == tomorrow)
}
fn is_overdue(&self, item: &Item) -> bool {
if item.checked {
return false;
}
let Some(due) = &item.due else {
return false;
};
let today = Local::now().date_naive();
self.parse_due_date(&due.date)
.is_some_and(|due_date| due_date < today)
}
fn has_no_date(&self, item: &Item) -> bool {
item.due.is_none()
}
fn is_due_within_7_days(&self, item: &Item) -> bool {
let Some(due) = &item.due else {
return false;
};
let today = Local::now().date_naive();
let end_date = today + chrono::Duration::days(7);
self.parse_due_date(&due.date)
.is_some_and(|due_date| due_date >= today && due_date < end_date)
}
fn is_due_on_specific_date(&self, item: &Item, month: u32, day: u32) -> bool {
let Some(due) = &item.due else {
return false;
};
self.parse_due_date(&due.date)
.is_some_and(|due_date| due_date.month() == month && due_date.day() == day)
}
fn parse_due_date(&self, date_str: &str) -> Option<NaiveDate> {
NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok()
}
fn has_label(&self, item: &Item, label_name: &str) -> bool {
let label_lower = label_name.to_lowercase();
item.labels.iter().any(|l| l.to_lowercase() == label_lower)
}
fn has_no_labels(&self, item: &Item) -> bool {
item.labels.is_empty()
}
fn in_project(&self, item: &Item, project_name: &str) -> bool {
self.context
.find_project_by_name(project_name)
.is_some_and(|project| project.id == item.project_id)
}
fn in_project_or_subproject(&self, item: &Item, project_name: &str) -> bool {
let project_ids = self.context.get_project_ids_with_subprojects(project_name);
project_ids.contains(&item.project_id.as_str())
}
fn in_section(&self, item: &Item, section_name: &str) -> bool {
let Some(section_id) = &item.section_id else {
return false;
};
self.context
.find_section_by_name(section_name)
.is_some_and(|section| §ion.id == section_id)
}
fn is_assigned_to(&self, item: &Item, target: &AssignedTarget) -> bool {
match target {
AssignedTarget::Me => {
let Some(current_uid) = self.context.current_user_id else {
return false;
};
item.responsible_uid.as_deref() == Some(current_uid)
}
AssignedTarget::Others => {
let Some(current_uid) = self.context.current_user_id else {
return false;
};
item.responsible_uid
.as_ref()
.is_some_and(|uid| uid != current_uid)
}
AssignedTarget::User(name) => {
let Some(collaborator) = self.context.find_collaborator_by_name(name) else {
return false;
};
item.responsible_uid.as_deref() == Some(collaborator.id.as_str())
}
}
}
fn is_assigned_by(&self, item: &Item, target: &AssignedTarget) -> bool {
match target {
AssignedTarget::Me => {
let Some(current_uid) = self.context.current_user_id else {
return false;
};
item.assigned_by_uid.as_deref() == Some(current_uid)
}
AssignedTarget::Others => {
let Some(current_uid) = self.context.current_user_id else {
return false;
};
item.assigned_by_uid
.as_ref()
.is_some_and(|uid| uid != current_uid)
}
AssignedTarget::User(name) => {
let Some(collaborator) = self.context.find_collaborator_by_name(name) else {
return false;
};
item.assigned_by_uid.as_deref() == Some(collaborator.id.as_str())
}
}
}
}
#[cfg(test)]
#[path = "evaluator_tests.rs"]
mod tests;