Functional Weave
Code in Rust

todo.parse-date-phrase@1.0.0

impl/rust.rs

5,064 bytes · the Rust implementation · view raw

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,
    }
}