use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_day_of_week::day_of_week; use super::todo_item::{Recurrence, TodoDraft}; use super::todo_item_validate_draft::todo_draft_to_value; use super::todo_normalise_tags::normalise_tags; use super::todo_parse_date_phrase::parse_date_phrase; // The longest phrase todo.parse-date-phrase accepts: "due in 3 days". const MAX_DATE_WORDS: usize = 4; struct Repeat { frequency: &'static str, interval: i64, weekday: Option, length: usize, } fn priority_of(word: &str) -> Option<&'static str> { match word { "!high" | "!h" | "!1" | "!!!" => Some("high"), "!medium" | "!med" | "!m" | "!2" | "!!" => Some("medium"), "!low" | "!l" | "!3" => Some("low"), "!none" => Some("none"), _ => None, } } /// Short words that are also ordinary title words ("Read the sun"): on their /// own they stay in the title; "on sun" or "due sat" still sets the date. fn is_guarded(word: &str) -> bool { matches!( word, "mon" | "tue" | "tues" | "wed" | "thu" | "thur" | "thurs" | "fri" | "sat" | "sun" | "tod" | "weekend" ) } fn is_weekday_word(word: &str) -> bool { matches!( word, "monday" | "mon" | "tuesday" | "tue" | "tues" | "wednesday" | "wed" | "thursday" | "thu" | "thur" | "thurs" | "friday" | "fri" | "saturday" | "sat" | "sunday" | "sun" ) } fn unit_frequency(word: &str) -> Option<&'static str> { match word { "day" | "days" => Some("daily"), "week" | "weeks" => Some("weekly"), "month" | "months" => Some("monthly"), "year" | "years" => Some("yearly"), _ => None, } } /// 1 to 999 in ASCII digits without a leading zero, or one...ten. fn count(word: &str) -> Option { let b = word.as_bytes(); if !b.is_empty() && b.len() <= 3 && b[0] != b'0' && b.iter().all(|c| c.is_ascii_digit()) { return Some(word.parse().unwrap()); } match word { "one" => Some(1), "two" => Some(2), "three" => Some(3), "four" => Some(4), "five" => Some(5), "six" => Some(6), "seven" => Some(7), "eight" => Some(8), "nine" => Some(9), "ten" => Some(10), _ => None, } } /// The repeat phrase starting with "every" at words[i], or None. fn repeat_at(words: &[&str], i: usize) -> Option { if i + 1 >= words.len() { return None; } let a = words[i + 1].to_ascii_lowercase(); if a == "weekday" || a == "weekdays" { return Some(Repeat { frequency: "weekdays", interval: 1, weekday: None, length: 2 }); } if let Some(frequency) = unit_frequency(&a) { return Some(Repeat { frequency, interval: 1, weekday: None, length: 2 }); } if is_weekday_word(&a) { return Some(Repeat { frequency: "weekly", interval: 1, weekday: Some(a), length: 2 }); } if i + 2 >= words.len() { return None; } let frequency = unit_frequency(&words[i + 2].to_ascii_lowercase())?; let interval = if a == "other" { 2 } else { count(&a)? }; Some(Repeat { frequency, interval, weekday: None, length: 3 }) } fn is_space(c: char) -> bool { matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{0B}' | '\u{0C}') } /// One typed line into a TodoDraft: #tags, a !priority, the first date phrase /// and the first "every ..." repeat come out; every other word stays in the /// title, in order, single-spaced. The title can come out empty. /// /// # Panics /// Panics when `today` is not a real ISO date. pub fn parse_quick_add(text: &str, today: &str) -> TodoDraft { // add_days(x, 0) is the date check: it panics with dates.add-days's message. add_days(today, 0); let words: Vec<&str> = text.split(is_space).filter(|w| !w.is_empty()).collect(); let mut title: Vec<&str> = Vec::new(); let mut tag_words: Vec = Vec::new(); let mut priority: Option<&'static str> = None; let mut due: Option = None; let mut repeat: Option = None; let mut i = 0; while i < words.len() { let word = words[i]; let lower = word.to_ascii_lowercase(); if repeat.is_none() && lower == "every" { if let Some(found) = repeat_at(&words, i) { i += found.length; repeat = Some(found); continue; } } if priority.is_none() { if let Some(p) = priority_of(&lower) { priority = Some(p); i += 1; continue; } } if word.chars().count() > 1 && word.starts_with('#') && !normalise_tags(&[word.to_string()]).is_empty() { tag_words.push(word.to_string()); i += 1; continue; } if due.is_none() { let mut taken = 0; let longest = MAX_DATE_WORDS.min(words.len() - i); for length in (1..=longest).rev() { if length == 1 && is_guarded(&lower) { continue; } if let Some(date) = parse_date_phrase(&words[i..i + length].join(" "), today) { due = Some(date); taken = length; break; } } if taken > 0 { i += taken; continue; } } title.push(word); i += 1; } let recurrence = repeat.map(|r| { let date = match due.take() { Some(d) => d, None if r.frequency == "weekdays" => { let dow = day_of_week(today); if dow <= 5 { today.to_string() } else { add_days(today, 8 - dow) } } None => match &r.weekday { Some(w) => parse_date_phrase(w, today).expect("a weekday is a date phrase"), None => today.to_string(), }, }; due = Some(date.clone()); Recurrence { frequency: r.frequency.to_string(), interval: r.interval, anchor: date } }); TodoDraft { title: title.join(" "), notes: None, priority: priority.unwrap_or("none").to_string(), due, tags: normalise_tags(&tag_words), recurrence, } } pub fn fune_vector(args: &[Value]) -> Value { todo_draft_to_value(&parse_quick_add(args[0].as_str(), args[1].as_str())) }