use regex::Regex;
use std::sync::LazyLock;
pub struct AppRegexes {
pub whitespace: Regex,
pub prefixes: Regex,
pub time_in_primary: Regex,
pub time_in_secondary: Regex,
pub time_in_now: Regex,
pub time_explicit_convert: Regex,
pub time_convert: Regex,
pub time_now_query: Regex,
pub time_place_suffix: Regex,
pub time_place_prefix: Regex,
pub time_place_simple_prefix: Regex,
pub time_place_suffix_cleanup: Regex,
pub conversion_pattern: Regex,
pub currency_symbol_prefix: Regex,
pub unit_pattern: Regex,
pub unit_pattern_no_space: Regex,
pub time_relative_simple: Regex,
pub time_relative_complex: Regex,
pub time_relative_phrase: Regex,
pub time_duration_offset: Regex,
pub time_in_duration: Regex,
pub unix_timestamp_raw: Regex,
pub unix_timestamp_explicit: Regex,
pub to_unix_conversion: Regex,
pub iso_date: Regex,
pub current_date_query: Regex,
pub today_query: Regex,
pub date_target_query: Regex,
pub weekday_only: Regex,
pub date_named_anchor: Regex,
pub date_compound_relative: Regex,
pub days_between: Regex,
pub math_prefix_cleanup: Regex,
pub math_the_cleanup: Regex,
pub math_wrappers: Vec<(Regex, &'static str)>,
pub math_word_operators: Vec<(Regex, &'static str)>,
}
pub static REGEXES: LazyLock<AppRegexes> = LazyLock::new(|| {
AppRegexes {
whitespace: Regex::new(r"\s+").expect("Invalid whitespace regex"),
prefixes: Regex::new(r"(?i)^(?:convert|how much is|what(?:'s|s| is))\s+")
.expect("Invalid prefix removal regex"),
time_in_primary: Regex::new(
r"(?i)^(?:what(?:'s| is) )?(?:the )?(?:current )?time (?:in|at) (.+)$",
)
.expect("Invalid primary time-in regex"),
time_in_secondary: Regex::new(
r"(?i)^(?:what time is it|whats the time|what is the time|current time|time now) (?:in|at) (.+)$",
)
.expect("Invalid secondary time-in regex"),
time_in_now: Regex::new(r"(?i)^(?:now|current time) in (.+)$")
.expect("Invalid time-now regex"),
time_explicit_convert: Regex::new(
r"(?i)^(midnight|noon|\d{1,2}(?::\d{2})?(?:\s*(?:am|pm))?)\s+(?:in\s+)?(.+?)\s+to\s+(.+?)(?:\s+time)?$",
)
.expect("Invalid explicit time conversion regex"),
time_convert: Regex::new(r"(?i)^(.+?) to (.+?)(?: time)?$")
.expect("Invalid time conversion regex"),
time_now_query: Regex::new(r"(?i)^(?:what(?:'s| is) )?(?:the )?(?:current )?time(?: now)?$")
.expect("Invalid time-now query regex"),
time_place_suffix: Regex::new(r"(?i)^(.+?)\s+(?:time|now|time now)$")
.expect("Invalid time-place suffix regex"),
time_place_prefix: Regex::new(r"(?i)^(?:time|current time|time now|now)\s+(?:in|at)\s+")
.expect("Invalid time-place prefix regex"),
time_place_simple_prefix: Regex::new(r"(?i)^(?:in|at)\s+")
.expect("Invalid simple time-place prefix regex"),
time_place_suffix_cleanup: Regex::new(r"(?i)\s+(?:time|now|time now)$")
.expect("Invalid time-place suffix cleanup regex"),
conversion_pattern: Regex::new(
r"(?i)^([\d.,]+)?\s*([a-zA-Z$€£¥₹₩₽₺₦₵₪฿]+(?:\s+[a-zA-Z]+)?)\s+(?:to|in|into|as|=)\s+([a-zA-Z$€£¥₹₩₽₺₦₵₪฿]+(?:\s+[a-zA-Z]+)?)$",
).expect("Invalid currency conversion pattern"),
currency_symbol_prefix: Regex::new(r"^([$€£¥₹₩₽₺₦₵₪฿])\s*([\d.,]+)")
.expect("Invalid currency symbol-prefix regex"),
unit_pattern: Regex::new(
r#"(?i)^(-?[\d.,]+)\s*([a-z°/µμ'"2-3²³]+(?:\s+[a-z]+(?:\s+[a-z]+)?)?)\s+(?:to|in|into|as|=)\s+([a-z°/µμ'"2-3²³]+(?:\s+[a-z]+(?:\s+[a-z]+)?)?)$"#,
)
.expect("Invalid unit conversion pattern"),
unit_pattern_no_space: Regex::new(
r#"(?i)^(-?[\d.,]+)([a-z0-9°/µμ'"²³]+)\s+(?:to|in|into|as)\s+([a-z0-9°/µμ'"/²³]+(?:\s+[a-z]+)?)$"#,
)
.expect("Invalid compact unit conversion pattern"),
time_relative_simple: Regex::new(
r"(?i)^(?:the\s+)?(today|now|tomorrow|yesterday|tmr|tmrw|yday)$",
)
.expect("Invalid simple time regex"),
time_relative_complex: Regex::new(
r"(?i)^(?:the\s+)?(date|day after tomorrow|day before yesterday|day after tmr|day before yday)$",
)
.expect("Invalid complex relative time regex"),
time_relative_phrase: Regex::new(
r"(?i)^(next|last|this|coming|upcoming)\s+(monday|mon|tuesday|tue|tues|wednesday|wed|thursday|thu|thur|thurs|friday|fri|saturday|sat|sunday|sun|week|month|year|weekend)$"
).expect("Invalid relative date phrase regex"),
time_duration_offset: Regex::new(
r"(?i)^(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?|seconds?)\s+(from now|ago|from today|from tomorrow|from yesterday|later)$"
).expect("Invalid duration offset regex"),
time_in_duration: Regex::new(r"(?i)^in\s+(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?|seconds?)$")
.expect("Invalid 'in duration' regex"),
unix_timestamp_raw: Regex::new(r"(?i)^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$")
.expect("Invalid raw unix timestamp regex"),
unix_timestamp_explicit: Regex::new(r"(?i)^(?:unix|timestamp|epoch)\s+(\d{10,13})$")
.expect("Invalid explicit unix regex"),
to_unix_conversion: Regex::new(r"(?i)^(.+?)\s+(?:to|in)\s+(?:unix|timestamp|epoch)$")
.expect("Invalid 'to unix' conversion regex"),
iso_date: Regex::new(r"^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2})?")
.expect("Invalid ISO date regex"),
current_date_query: Regex::new(r"(?i)^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$")
.expect("Invalid current date query regex"),
today_query: Regex::new(
r"(?i)^(?:(?:what(?:'s|s| is))\s+today|(?:what(?:'s|s| is))\s+today'?s date|what day is it|what date is it|todays date|today'?s date|date today|current date|(?:what(?:'s|s| is))\s+the\s+date|what is the date(?: today)?)$"
).expect("Invalid natural language today query regex"),
date_target_query: Regex::new(
r"(?i)^(?:what\s+(?:day|date)\s+is|date\s+for|day\s+of)\s+(.+)$|^(?:when\s+is)\s+((?:(?:next|last|this|coming|upcoming)\s+(?:monday|mon|tuesday|tue|tues|wednesday|wed|thursday|thu|thur|thurs|friday|fri|saturday|sat|sunday|sun|week|month|year|weekend))|today|tomorrow|yesterday|tmr|tmrw|yday|day\s+after\s+(?:tomorrow|tmr)|day\s+before\s+(?:yesterday|yday)|(?:the\s+)?(?:start|beginning|end)\s+of\s+(?:the\s+)?(?:week|month|year)|\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2})?)$",
)
.expect("Invalid natural language date-target regex"),
weekday_only: Regex::new(
r"(?i)^(?:the\s+)?(?:on\s+)?(monday|mon|tuesday|tue|tues|wednesday|wed|thursday|thu|thur|thurs|friday|fri|saturday|sat|sunday|sun)$",
)
.expect("Invalid weekday-only regex"),
date_named_anchor: Regex::new(
r"(?i)^(?:the\s+)?(start|beginning|end)\s+of\s+(?:the\s+)?(week|month|year)$",
)
.expect("Invalid named date-anchor regex"),
date_compound_relative: Regex::new(
r"(?i)^(?:the\s+)?(week|month|year)\s+(after\s+next|before\s+last)$",
)
.expect("Invalid compound relative date regex"),
days_between: Regex::new(
r"(?i)^(?:days?\s+)?(?:between|from)\s+(.+?)\s+(?:to|and|until)\s+(.+)$",
)
.expect("Invalid days-between regex"),
math_prefix_cleanup: Regex::new(r"(?i)^(?:(?:what(?:'s|s| is))\s+the\s+|(?:what(?:'s|s| is))\s+|calculate\s+)").unwrap(),
math_the_cleanup: Regex::new(r"(?i)^the\s+").unwrap(),
math_wrappers: vec![
(Regex::new(r"(?i)^square root of (.+)$").unwrap(), "sqrt($1)"),
(Regex::new(r"(?i)^cube root of (.+)$").unwrap(), "cbrt($1)"),
(Regex::new(r"(?i)^log of (.+)$").unwrap(), "log($1)"),
(Regex::new(r"(?i)^sine of (.+)$").unwrap(), "sin($1)"),
(Regex::new(r"(?i)^cosine of (.+)$").unwrap(), "cos($1)"),
(Regex::new(r"(?i)^tangent of (.+)$").unwrap(), "tan($1)"),
(Regex::new(r"(?i)^absolute value of (.+)$").unwrap(), "abs($1)"),
(Regex::new(r"(?i)^factorial of (.+)$").unwrap(), "($1)!"),
(Regex::new(r"(?i)^half of (.+)$").unwrap(), "($1) / 2"),
(Regex::new(r"(?i)^double (.+)$").unwrap(), "2 * ($1)"),
(Regex::new(r"(?i)^triple (.+)$").unwrap(), "3 * ($1)"),
(Regex::new(r"(?i)^one third of (.+)$").unwrap(), "($1) / 3"),
(Regex::new(r"(?i)^one quarter of (.+)$").unwrap(), "($1) / 4"),
(Regex::new(r"(?i)^remainder of (.+?) divided by (.+)$").unwrap(), "($1) % ($2)"),
(Regex::new(r"(?i)^(.+?) to the power of (.+)$").unwrap(), "($1) ^ ($2)"),
(Regex::new(r"(?i)^(.+?) raised to (.+)$").unwrap(), "($1) ^ ($2)"),
(Regex::new(r"(?i)^(.+?) squared$").unwrap(), "($1) ^ 2"),
(Regex::new(r"(?i)^(.+?) cubed$").unwrap(), "($1) ^ 3"),
(Regex::new(r"(?i)^(\d+(?:\.\d+)?) percent of (.+)$").unwrap(), "($1%) * ($2)"),
(Regex::new(r"(?i)^(.+?%) of (.+)$").unwrap(), "($1) * ($2)"),
],
math_word_operators: vec![
(Regex::new(r"(?i)\bdivided by\b").unwrap(), "/"),
(Regex::new(r"(?i)\btimes\b").unwrap(), "*"),
(Regex::new(r"(?i)\bplus\b").unwrap(), "+"),
(Regex::new(r"(?i)\bminus\b").unwrap(), "-"),
],
}
});
pub fn date_intent_regexes() -> [&'static Regex; 16] {
[
®EXES.time_relative_simple,
®EXES.time_relative_complex,
®EXES.time_relative_phrase,
®EXES.time_duration_offset,
®EXES.time_in_duration,
®EXES.unix_timestamp_raw,
®EXES.unix_timestamp_explicit,
®EXES.to_unix_conversion,
®EXES.iso_date,
®EXES.current_date_query,
®EXES.today_query,
®EXES.date_target_query,
®EXES.weekday_only,
®EXES.date_named_anchor,
®EXES.date_compound_relative,
®EXES.days_between,
]
}
pub fn normalize_whitespace(input: &str) -> String {
let trimmed = input.trim();
REGEXES.whitespace.replace_all(trimmed, " ").into_owned()
}
pub fn normalize_conversion_input(input: &str) -> String {
let normalized_ws = normalize_whitespace(input);
let normalized = REGEXES.prefixes.replace(&normalized_ws, "").into_owned();
REGEXES
.currency_symbol_prefix
.replace(&normalized, "$2 $1")
.into_owned()
}
pub fn normalize_time_place(input: &str) -> String {
let normalized = normalize_whitespace(input);
let no_prefix = REGEXES
.time_place_prefix
.replace(&normalized, "")
.into_owned();
let no_simple_prefix = REGEXES
.time_place_simple_prefix
.replace(&no_prefix, "")
.into_owned();
REGEXES
.time_place_suffix_cleanup
.replace(&no_simple_prefix, "")
.trim()
.to_string()
}