tudu 1.1.0

Manage your tasks with a simple but powerful system
Documentation
use crate::storage::{create_filepath, parse_task_file, write_tasks_to_file};
use crate::TuduDate;
use crate::TuduError;

#[derive(Eq, PartialEq, Debug)]
pub enum Command {
    Add(AddCommand),
    Remove(RemoveCommand),
    Set(SetCommand),
    Edit(EditCommand),
    View(ViewCommand),
    Help,
}

#[derive(Eq, PartialEq, Debug, Clone)]
pub enum TaskState {
    NotStarted,
    Started,
    Complete,
    Forwarded,
    Ignored,
}

#[derive(Eq, PartialEq, Debug)]
pub struct AddCommand {
    pub task: String,
    pub date: Option<TuduDate>,
}

#[derive(Eq, PartialEq, Debug)]
pub struct RemoveCommand {
    pub index: usize,
    pub date: Option<TuduDate>,
}

#[derive(Eq, PartialEq, Debug)]
pub struct SetCommand {
    pub index: usize,
    pub date: Option<TuduDate>,
    pub state: TaskState,
}

#[derive(Eq, PartialEq, Debug)]
pub struct ViewCommand {
    pub date: TuduDate,
}

#[derive(Eq, PartialEq, Debug)]
pub struct EditCommand {
    pub index: usize,
    pub task: String,
    pub date: Option<TuduDate>,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Task {
    pub task: String,
    pub state: TaskState,
}

impl Task {
    pub fn new(task: String, state: TaskState) -> Task {
        Task { task, state }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct TaskList<'a> {
    tasks: Vec<Task>,
    date: &'a TuduDate,
}

impl TaskList<'_> {
    pub fn for_date(date: &TuduDate) -> Result<TaskList, TuduError> {
        let filename = date.to_filename();
        let filepath = create_filepath(&filename)?;

        match parse_task_file(&filepath) {
            Ok(tasks) => Ok(TaskList { tasks, date }),
            Err(TuduError::NoTaskFile) => Ok(TaskList::empty(date)),
            Err(err) => Err(err),
        }
    }

    pub fn add_task(&mut self, new_task: Task) {
        self.tasks.push(new_task);
    }

    pub fn set_task_state(
        &mut self,
        index: usize,
        desired_state: TaskState,
    ) -> Result<(), TuduError> {
        let corrected_index = index - 1;

        match self.tasks.get_mut(corrected_index) {
            Some(task) => Ok(task.state = desired_state),
            None => Err(TuduError::InvalidIndex),
        }
    }

    pub fn remove_task(&mut self, index: usize) -> Result<(), TuduError> {
        let corrected_index = index - 1;

        if corrected_index > self.tasks.len() {
            return Err(TuduError::InvalidIndex);
        }

        self.tasks.remove(corrected_index);

        Ok(())
    }

    pub fn edit_task(&mut self, index: usize, new_task: String) -> Result<(), TuduError> {
        let corrected_index = index - 1;

        match self.tasks.get_mut(corrected_index) {
            Some(task) => Ok(task.task = new_task),
            None => Err(TuduError::InvalidIndex),
        }
    }

    pub fn get_formatted_tasks(&self) -> String {
        if self.tasks.len() == 0 {
            return format!("There are no tasks for this date");
        }

        let mut formatted_output = String::new();

        self.tasks.iter().enumerate().for_each(|(index, task)| {
            let icon = match task.state {
                TaskState::NotStarted => "",
                TaskState::Started => "",
                TaskState::Complete => "",
                TaskState::Forwarded => "",
                TaskState::Ignored => "x",
            };
            let description = &task.task;
            let formatted_index = index + 1;

            let formatted = format!("{formatted_index}    {icon} - {description}\n");

            formatted_output.push_str(&formatted);
        });

        formatted_output
    }

    fn empty(date: &TuduDate) -> TaskList {
        TaskList {
            tasks: Vec::new(),
            date,
        }
    }

    pub fn write_to_file(&self) -> Result<(), TuduError> {
        let filename = self.date.to_filename();

        let filepath = create_filepath(&filename)?;

        write_tasks_to_file(&filepath, &self.tasks)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn create_from_file_when_no_file_creates_empty_task_list() {
        let date = TuduDate::new(2023, 12, 13);

        let expected_task_list = TaskList::empty(&date);

        let task_list = TaskList::for_date(&date).unwrap();

        assert_eq!(task_list, expected_task_list);
    }

    #[test]
    fn add_task_adds_task_to_end_of_list() {
        let date = TuduDate::new(1, 1, 2023);
        let first_task = Task::new(String::from("First task"), TaskState::Complete);
        let second_task = Task::new(String::from("Second task"), TaskState::NotStarted);

        let expected_task_list = TaskList {
            date: &date,
            tasks: vec![first_task.clone(), second_task.clone()],
        };

        let mut task_list = TaskList {
            date: &date,
            tasks: vec![first_task.clone()],
        };

        task_list.add_task(second_task.clone());

        assert_eq!(task_list.tasks, expected_task_list.tasks);
    }

    #[test]
    fn set_task_at_index_edits_that_task() {
        let date = TuduDate::new(1, 1, 2023);
        let first_task = Task::new(String::from("AAA"), TaskState::Complete);
        let second_task = Task::new(String::from("BBB"), TaskState::Complete);

        let expected_task_list = TaskList {
            tasks: vec![first_task.clone(), second_task.clone()],
            date: &date,
        };

        let mut task_list = TaskList {
            date: &date,
            tasks: vec![
                first_task.clone(),
                Task::new(String::from("BBB"), TaskState::NotStarted),
            ],
        };

        task_list.set_task_state(2, TaskState::Complete).unwrap();

        assert_eq!(task_list.tasks, expected_task_list.tasks);
    }

    #[test]
    fn set_task_at_index_if_no_task_at_index_throws_error() {
        let date = TuduDate::new(1, 1, 2023);

        let mut task_list = TaskList {
            date: &date,
            tasks: vec![Task::new(String::from("AAA"), TaskState::NotStarted)],
        };

        let expected_error = TuduError::InvalidIndex;

        let result = task_list.set_task_state(2, TaskState::Complete);

        assert!(result.is_err());
        assert_eq!(result.err().unwrap(), expected_error);
    }

    #[test]
    fn remove_task_at_index_removes_that_task() {
        let date = TuduDate::new(1, 1, 2023);
        let first_task = Task::new(String::from("AAA"), TaskState::Complete);
        let second_task = Task::new(String::from("BBB"), TaskState::Complete);

        let expected_task_list = TaskList {
            tasks: vec![first_task.clone()],
            date: &date,
        };

        let mut task_list = TaskList {
            date: &date,
            tasks: vec![first_task, second_task],
        };

        task_list.remove_task(2);

        assert_eq!(task_list.tasks, expected_task_list.tasks);
    }

    #[test]
    fn edit_task_at_index_edits_that_task() {
        let date = TuduDate::new(1, 1, 2023);
        let first_task = Task::new(String::from("AAA"), TaskState::Complete);
        let second_task = Task::new(String::from("BBB"), TaskState::Complete);

        let expected_task_list = TaskList {
            tasks: vec![first_task.clone(), second_task.clone()],
            date: &date,
        };

        let mut task_list = TaskList {
            date: &date,
            tasks: vec![
                first_task.clone(),
                Task::new(String::from("CCC"), TaskState::Complete),
            ],
        };

        task_list.edit_task(2, String::from("BBB")).unwrap();

        assert_eq!(task_list.tasks, expected_task_list.tasks);
    }

    #[test]
    fn edit_task_at_index_if_no_task_at_index_throws_error() {
        let date = TuduDate::new(1, 1, 2023);

        let mut task_list = TaskList {
            date: &date,
            tasks: vec![Task::new(String::from("AAA"), TaskState::NotStarted)],
        };

        let expected_error = TuduError::InvalidIndex;

        let result = task_list.edit_task(2, String::from("BBB"));

        assert!(result.is_err());
        assert_eq!(result.err().unwrap(), expected_error);
    }

    #[test]
    fn get_formatted_tasks_formats_tasks_correctly() {
        let tasks = vec![
            Task::new(String::from("This task is started"), TaskState::Started),
            Task::new(String::from("This one is completed"), TaskState::Complete),
            Task::new(String::from("Didn't like this one"), TaskState::Ignored),
            Task::new(String::from("This one's for later"), TaskState::Forwarded),
            Task::new(String::from("Patience is a virtue"), TaskState::NotStarted),
        ];

        let task_list = TaskList {
            date: &TuduDate::new(1, 1, 2023),
            tasks,
        };

        let expected_formatting = String::from(
            "1    ◐ - This task is started
2    ● - This one is completed
3    x - Didn't like this one
4    ► - This one's for later
5    ◯ - Patience is a virtue\n",
        );

        let formatted = task_list.get_formatted_tasks();

        assert_eq!(formatted, expected_formatting);
    }

    #[test]
    fn get_formatted_tasks_when_empty_gives_helpful_message() {
        let date = TuduDate::today();
        let task_list = TaskList::empty(&date);

        let expected_message = "There are no tasks for this date";

        let message = task_list.get_formatted_tasks();

        assert_eq!(message, expected_message);
    }
}