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, days_in_month}; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months; ← from dates.add-months ^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
fn weekday_number(word: &str) -> Option<i64> {
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<i64> {
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<i64> {
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<String> {
// 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,
}
}