from typing import Any, Dict from .todo_item_types import Todo, TodoValidation from .todo_item_validate_draft import draft_errors, is_iso_date, trim_space _FIELD_ORDER = ("id", "title", "notes", "priority", "due", "tags", "recurrence", "createdAt", "completedAt", "order") def is_utc_timestamp(text: Any) -> bool: """YYYY-MM-DDTHH:MM:SS, an optional 1-9 digit fraction, then Z.""" if not isinstance(text, str) or len(text) < 20: return False if not is_iso_date(text[0:10]) or text[10] != "T" or text[13] != ":" or text[16] != ":": return False def two(at: int, most: int) -> bool: a, b = text[at], text[at + 1] return "0" <= a <= "9" and "0" <= b <= "9" and int(a + b) <= most if not (two(11, 23) and two(14, 59) and two(17, 59)): return False rest = text[19:] if rest.startswith("."): n = 1 while n < len(rest) and "0" <= rest[n] <= "9": n += 1 if n == 1 or n > 10: return False rest = rest[n:] return rest == "Z" def validate_todo(todo: Todo) -> TodoValidation: """Check a stored todo: the draft rules plus id, trimmed title, timestamps, completion and order.""" found: Dict[str, str] = dict(draft_errors(todo)) if trim_space(todo.id) == "": found["id"] = "Every todo needs an id." if "title" not in found and trim_space(todo.title) != todo.title: found["title"] = "Remove the spaces around the title." if not is_utc_timestamp(todo.created_at): found["createdAt"] = "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z." if todo.done and todo.completed_at is None: found["completedAt"] = "A done todo needs the time it was completed." elif not todo.done and todo.completed_at is not None: found["completedAt"] = "An open todo cannot have a completion time." elif todo.completed_at is not None and not is_utc_timestamp(todo.completed_at): found["completedAt"] = "Record when it was completed as a UTC timestamp, e.g. 2026-09-28T09:30:00Z." order = todo.order if isinstance(order, bool) or not isinstance(order, int) or order < 0: found["order"] = "Order must be a whole number, 0 or more." errors = {field: found[field] for field in _FIELD_ORDER if field in found} return TodoValidation(valid=len(errors) == 0, errors=errors)