use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TodoStatus {
Pending,
InProgress,
Completed,
}
impl TodoStatus {
pub fn is_completed(&self) -> bool {
matches!(self, TodoStatus::Completed)
}
pub fn is_active(&self) -> bool {
matches!(self, TodoStatus::Pending | TodoStatus::InProgress)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
pub id: String,
pub content: String,
pub status: TodoStatus,
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl TodoItem {
pub fn new(id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
id: id.into(),
content: content.into(),
status: TodoStatus::Pending,
created_at: chrono::Utc::now(),
}
}
pub fn complete(&mut self) {
self.status = TodoStatus::Completed;
}
pub fn start(&mut self) {
self.status = TodoStatus::InProgress;
}
pub fn reset(&mut self) {
self.status = TodoStatus::Pending;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoList {
pub id: String,
pub name: String,
pub items: Vec<TodoItem>,
}
impl TodoList {
pub fn new(name: impl Into<String>) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name: name.into(),
items: Vec::new(),
}
}
pub fn add(&mut self, content: impl Into<String>) -> &TodoItem {
let item = TodoItem::new(uuid::Uuid::new_v4().to_string(), content);
self.items.push(item);
self.items.last().unwrap()
}
pub fn complete(&mut self, id: &str) -> Result<(), TodoError> {
let item = self
.items
.iter_mut()
.find(|item| item.id == id)
.ok_or_else(|| TodoError::NotFound(id.to_string()))?;
item.complete();
Ok(())
}
pub fn start(&mut self, id: &str) -> Result<(), TodoError> {
let item = self
.items
.iter_mut()
.find(|item| item.id == id)
.ok_or_else(|| TodoError::NotFound(id.to_string()))?;
item.start();
Ok(())
}
pub fn reset(&mut self, id: &str) -> Result<(), TodoError> {
let item = self
.items
.iter_mut()
.find(|item| item.id == id)
.ok_or_else(|| TodoError::NotFound(id.to_string()))?;
item.reset();
Ok(())
}
pub fn remove(&mut self, id: &str) -> Result<(), TodoError> {
let index = self
.items
.iter()
.position(|item| item.id == id)
.ok_or_else(|| TodoError::NotFound(id.to_string()))?;
self.items.remove(index);
Ok(())
}
pub fn get(&self, id: &str) -> Option<&TodoItem> {
self.items.iter().find(|item| item.id == id)
}
pub fn filter_by_status(&self, status: TodoStatus) -> Vec<&TodoItem> {
self.items
.iter()
.filter(|item| item.status == status)
.collect()
}
pub fn count_by_status(&self) -> HashMap<TodoStatus, usize> {
let mut counts = HashMap::new();
for item in &self.items {
*counts.entry(item.status).or_insert(0) += 1;
}
counts
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn completed_count(&self) -> usize {
self.items.iter().filter(|item| item.status.is_completed()).count()
}
pub fn completion_percentage(&self) -> f64 {
if self.is_empty() {
return 0.0;
}
(self.completed_count() as f64 / self.len() as f64) * 100.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TodoError {
NotFound(String),
InvalidInput(String),
}
impl std::fmt::Display for TodoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TodoError::NotFound(id) => write!(f, "Todo item not found: {}", id),
TodoError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}
impl std::error::Error for TodoError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_todo_status() {
let status = TodoStatus::Pending;
assert!(!status.is_completed());
assert!(status.is_active());
let status = TodoStatus::InProgress;
assert!(!status.is_completed());
assert!(status.is_active());
let status = TodoStatus::Completed;
assert!(status.is_completed());
assert!(!status.is_active());
}
#[test]
fn test_todo_item_creation() {
let item = TodoItem::new("123", "Test task");
assert_eq!(item.id, "123");
assert_eq!(item.content, "Test task");
assert_eq!(item.status, TodoStatus::Pending);
}
#[test]
fn test_todo_item_complete() {
let mut item = TodoItem::new("123", "Test task");
item.complete();
assert!(item.status.is_completed());
}
#[test]
fn test_todo_item_start() {
let mut item = TodoItem::new("123", "Test task");
item.start();
assert_eq!(item.status, TodoStatus::InProgress);
}
#[test]
fn test_todo_item_reset() {
let mut item = TodoItem::new("123", "Test task");
item.complete();
item.reset();
assert_eq!(item.status, TodoStatus::Pending);
}
#[test]
fn test_todo_list_creation() {
let list = TodoList::new("My Tasks");
assert_eq!(list.name, "My Tasks");
assert!(!list.id.is_empty());
assert!(list.is_empty());
}
#[test]
fn test_todo_list_add() {
let mut list = TodoList::new("My Tasks");
let item = list.add("Task 1");
assert_eq!(item.content, "Task 1");
assert_eq!(list.len(), 1);
}
#[test]
fn test_todo_list_complete() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
let id = list.items[0].id.clone();
let result = list.complete(&id);
assert!(result.is_ok());
assert!(list.items[0].status.is_completed());
}
#[test]
fn test_todo_list_complete_not_found() {
let mut list = TodoList::new("My Tasks");
let result = list.complete("nonexistent");
assert!(matches!(result, Err(TodoError::NotFound(_))));
}
#[test]
fn test_todo_list_remove() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
let id = list.items[0].id.clone();
let result = list.remove(&id);
assert!(result.is_ok());
assert!(list.is_empty());
}
#[test]
fn test_todo_list_get() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
let id = list.items[0].id.clone();
let item = list.get(&id);
assert!(item.is_some());
assert_eq!(item.unwrap().content, "Task 1");
let not_found = list.get("nonexistent");
assert!(not_found.is_none());
}
#[test]
fn test_todo_list_filter_by_status() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
list.add("Task 2");
let id = list.items[0].id.clone();
list.complete(&id).unwrap();
let pending = list.filter_by_status(TodoStatus::Pending);
assert_eq!(pending.len(), 1);
let completed = list.filter_by_status(TodoStatus::Completed);
assert_eq!(completed.len(), 1);
}
#[test]
fn test_todo_list_count_by_status() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
list.add("Task 2");
list.add("Task 3");
let id = list.items[0].id.clone();
list.complete(&id).unwrap();
let counts = list.count_by_status();
assert_eq!(*counts.get(&TodoStatus::Pending).unwrap_or(&0), 2);
assert_eq!(*counts.get(&TodoStatus::Completed).unwrap_or(&0), 1);
}
#[test]
fn test_todo_list_completed_count() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
list.add("Task 2");
assert_eq!(list.completed_count(), 0);
let id = list.items[0].id.clone();
list.complete(&id).unwrap();
assert_eq!(list.completed_count(), 1);
}
#[test]
fn test_todo_list_completion_percentage() {
let mut list = TodoList::new("My Tasks");
assert_eq!(list.completion_percentage(), 0.0);
list.add("Task 1");
list.add("Task 2");
let id = list.items[0].id.clone();
list.complete(&id).unwrap();
assert_eq!(list.completion_percentage(), 50.0);
}
#[test]
fn test_todo_error_display() {
let error = TodoError::NotFound("123".to_string());
assert_eq!(format!("{}", error), "Todo item not found: 123");
let error = TodoError::InvalidInput("test".to_string());
assert_eq!(format!("{}", error), "Invalid input: test");
}
#[test]
fn test_todo_list_start() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
let id = list.items[0].id.clone();
let result = list.start(&id);
assert!(result.is_ok());
assert_eq!(list.items[0].status, TodoStatus::InProgress);
}
#[test]
fn test_todo_list_reset() {
let mut list = TodoList::new("My Tasks");
list.add("Task 1");
let id = list.items[0].id.clone();
list.complete(&id).unwrap();
assert!(list.items[0].status.is_completed());
list.reset(&id).unwrap();
assert_eq!(list.items[0].status, TodoStatus::Pending);
}
}