use super::funejson::Value; use super::dates_add_days::{add_days, days_in_month}; use super::dates_add_months::add_months; use super::dates_day_of_week::day_of_week; fn weekday_number(word: &str) -> Option { match word { "monday" | "mon" => Some(1), "tuesday" | "tue" | "tues" => Some(2), "wednesday" | "wed" => Some(3), "thursday" | "thu" | "thur" | "thurs" => Some(4), "friday" | "fri" => Some(5), "saturday" | "sat" => Some(6), "sunday" | "sun" => Some(7), _ => None, } } fn number_word(word: &str) -> Option { match word { "a" | "an" | "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, } } fn unit(word: &str) -> Option<&'static str> { match word { "day" | "days" => Some("day"), "week" | "weeks" => Some("week"), "month" | "months" => Some("month"), "year" | "years" => Some("year"), _ => None, } } fn is_space(c: char) -> bool { matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{0B}' | '\u{0C}') } fn is_digits(text: &str) -> bool { !text.is_empty() && text.bytes().all(|b| b.is_ascii_digit()) } /// YYYY-MM-DD naming a day that exists; anything else is simply not a date phrase. fn is_real_date(word: &str) -> bool { let b = word.as_bytes(); if b.len() != 10 || b[4] != b'-' || b[7] != b'-' { return false; } if !is_digits(&word[0..4]) || !is_digits(&word[5..7]) || !is_digits(&word[8..10]) { return false; } let year: i64 = word[0..4].parse().unwrap(); let month: i64 = word[5..7].parse().unwrap(); let day: i64 = word[8..10].parse().unwrap(); year >= 1 && (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month) } /// 1 to 999 in ASCII digits without a leading zero, or a/an/one...ten. fn count(word: &str) -> Option { if is_digits(word) && word.len() <= 3 && !word.starts_with('0') { return Some(word.parse().unwrap()); } number_word(word) } /// Days from today to the next given weekday, 1 to 7: never today itself. fn days_until(weekday: i64, today: i64) -> i64 { (weekday - today + 6) % 7 + 1 } /// The date a typed phrase names, counted from `today`, or None when the whole /// text is not one of the phrases in the README. /// /// # Panics /// Panics when `today` is not a real ISO date, or the answer would fall /// outside 0001-9999. pub fn parse_date_phrase(text: &str, today: &str) -> Option { // add_days(x, 0) is the date check: it panics with dates.add-days's message. add_days(today, 0); let lowered = text.to_ascii_lowercase(); let mut words: Vec<&str> = lowered.split(is_space).filter(|w| !w.is_empty()).collect(); if words.len() > 1 && (words[0] == "on" || words[0] == "due") { words.remove(0); } let dow = day_of_week(today); match words.len() { 1 => { let word = words[0]; match word { "today" | "tod" => Some(today.to_string()), "tomorrow" | "tmr" | "tmrw" => Some(add_days(today, 1)), "yesterday" => Some(add_days(today, -1)), "weekend" => Some(if dow >= 6 { today.to_string() } else { add_days(today, 6 - dow) }), _ => match weekday_number(word) { Some(weekday) => Some(add_days(today, days_until(weekday, dow))), None if is_real_date(word) => Some(word.to_string()), None => None, }, } } 2 => { let (first, second) = (words[0], words[1]); if first == "this" && second == "weekend" { return Some(if dow >= 6 { today.to_string() } else { add_days(today, 6 - dow) }); } if first != "next" { return None; } let next_monday = 8 - dow; match second { "week" => Some(add_days(today, next_monday)), "month" => Some(add_months(&format!("{}01", &today[0..8]), 1)), "year" => Some(add_months(&format!("{}01-01", &today[0..5]), 12)), _ => weekday_number(second).map(|weekday| add_days(today, next_monday + weekday - 1)), } } 3 if words[0] == "in" => { let n = count(words[1])?; match unit(words[2])? { "day" => Some(add_days(today, n)), "week" => Some(add_days(today, 7 * n)), "month" => Some(add_months(today, n)), _ => Some(add_months(today, 12 * n)), } } _ => None, } } pub fn fune_vector(args: &[Value]) -> Value { match parse_date_phrase(args[0].as_str(), args[1].as_str()) { Some(date) => Value::str(&date), None => Value::Null, } }