use super::funejson::Value; use super::collections_search_text::fold_ascii; use super::collections_sort_by::sort_by; use super::todo_item::{todo_from_value, todos_to_value, Todo}; use super::todo_item_validate_todo::is_utc_timestamp; /// Sort passes, least significant first; the last is the primary key and takes the direction. fn passes(by: &str) -> Option<&'static [(&'static str, &'static str)]> { match by { "manual" => Some(&[("created", "asc"), ("order", "dir")]), "due" => Some(&[("order", "asc"), ("rank", "desc"), ("due", "dir")]), "priority" => Some(&[("order", "asc"), ("due", "asc"), ("rank", "dir")]), "created" => Some(&[("order", "asc"), ("created", "dir")]), "title" => Some(&[("order", "asc"), ("title", "dir")]), _ => None, } } fn rank(priority: &str) -> i64 { match priority { "none" => 0, "low" => 1, "medium" => 2, "high" => 3, other => panic!("unknown priority \"{}\": expected none, low, medium or high", other), } } /// A creation time that sorts as text: the fraction padded to nine digits, so /// "...:00Z" comes before "...:00.5Z", where the raw strings put "." first. fn created_key(created_at: &str) -> String { if !is_utc_timestamp(created_at) { panic!("createdAt \"{}\" is not a UTC timestamp, e.g. 2026-09-28T09:30:00Z", created_at); } let fraction = if created_at.len() > 20 { &created_at[20..created_at.len() - 1] } else { "" }; format!("{}.{:0<9}", &created_at[..19], fraction) } /// Todos in list order for the chosen key: stable, built from /// collections.sort-by passes from the least to the most significant key; the /// direction flips only the primary key, and no-date todos stay last. /// /// # Panics /// Panics on an unknown key or direction, an unknown priority (due and /// priority sorts) or a createdAt that is not a UTC timestamp (manual and /// created sorts). pub fn sort_todos(todos: &[Todo], by: &str, direction: &str) -> Vec { let passes = match passes(by) { Some(p) => p, None => panic!("unknown sort key \"{}\": expected manual, due, priority, created or title", by), }; if direction != "asc" && direction != "desc" { panic!("direction must be \"asc\" or \"desc\", received \"{}\"", direction); } let uses = |field: &str| passes.iter().any(|(f, _)| *f == field); let mut records: Vec = todos .iter() .enumerate() .map(|(i, todo)| { let mut fields: Vec<(&str, Value)> = vec![("i", Value::Int(i as i64)), ("order", Value::Int(todo.order))]; if uses("created") { fields.push(("created", Value::Str(created_key(&todo.created_at)))); } if uses("rank") { fields.push(("rank", Value::Int(rank(&todo.priority)))); } if uses("due") { fields.push(("due", todo.due.as_deref().map(Value::str).unwrap_or(Value::Null))); } if uses("title") { fields.push(("title", Value::Str(fold_ascii(&todo.title)))); } Value::obj(fields) }) .collect(); for (field, d) in passes { records = sort_by(&records, field, if *d == "dir" { direction } else { d }); } records.iter().map(|r| todos[r.get("i").as_i64() as usize].clone()).collect() } pub fn fune_vector(args: &[Value]) -> Value { let todos: Vec = args[0].as_arr().iter().map(todo_from_value).collect(); todos_to_value(&sort_todos(&todos, args[1].as_str(), args[2].as_str())) }