Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::add_days; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week; ← from dates.day-of-week ^1.0.0 · built alongside by fune
use super::todo_item::{Recurrence, TodoDraft}; ← from todo.item ^1.0.0 · built alongside by fune
use super::todo_item_validate_draft::todo_draft_to_value;
use super::todo_normalise_tags::normalise_tags; ← from todo.normalise-tags ^1.0.0 · built alongside by fune
use super::todo_parse_date_phrase::parse_date_phrase; ← from todo.parse-date-phrase ^1.0.0 · built alongside by fune
// 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<String>,
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<i64> {
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<Repeat> {
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<String> = Vec::new();
let mut priority: Option<&'static str> = None;
let mut due: Option<String> = None;
let mut repeat: Option<Repeat> = 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()))
}