#[derive(Debug, Clone, PartialEq)]
pub struct Reminder {
pub connective: Option<&'static str>,
pub body: String,
}
fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
if s.len() >= prefix.len()
&& s.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
{
Some(&s[prefix.len()..])
} else {
None
}
}
pub fn parse_reminder(description: &str) -> Option<Reminder> {
let mut rest = description.trim();
for prefix in [
"please ",
"can you ",
"could you ",
"would you ",
"will you ",
] {
if let Some(r) = strip_prefix_ci(rest, prefix) {
rest = r.trim_start();
}
}
let after_verb = strip_prefix_ci(rest, "remind me ")
.or_else(|| strip_prefix_ci(rest, "remind us "))
.or_else(|| strip_prefix_ci(rest, "reminder: "))
.or_else(|| strip_prefix_ci(rest, "reminder "))?;
let connectives: [(&str, &'static str); 4] = [
("to ", "to"),
("about ", "about"),
("of ", "of"),
("that ", "that"),
];
for (prefix, conn) in connectives {
if let Some(body) = strip_prefix_ci(after_verb, prefix) {
return build_reminder(body, Some(conn));
}
}
build_reminder(after_verb, None)
}
fn build_reminder(raw_body: &str, connective: Option<&'static str>) -> Option<Reminder> {
let raw_body = raw_body.trim().trim_end_matches(['?', '.', '!']).trim();
if raw_body.is_empty() {
return None;
}
let first_word = raw_body
.split_whitespace()
.next()
.unwrap_or("")
.to_lowercase();
if matches!(
first_word.as_str(),
"what" | "when" | "where" | "who" | "whose" | "which" | "how" | "why"
) {
return None;
}
Some(Reminder {
connective,
body: flip_pronouns(raw_body),
})
}
fn flip_pronouns(text: &str) -> String {
text.split_whitespace()
.map(|token| {
let core_start = token.find(|c: char| c.is_alphanumeric() || c == '\'');
let Some(cs) = core_start else {
return token.to_string();
};
let core_end = token
.rfind(|c: char| c.is_alphanumeric() || c == '\'')
.map(|i| i + token[i..].chars().next().map_or(1, char::len_utf8))
.unwrap_or(token.len());
let (lead, rest) = token.split_at(cs);
let (core, tail) = rest.split_at(core_end - cs);
let flipped = match core.to_lowercase().as_str() {
"i" => Some("you"),
"i'm" => Some("you're"),
"i'll" => Some("you'll"),
"i've" => Some("you've"),
"me" => Some("you"),
"my" => Some("your"),
"mine" => Some("yours"),
"myself" => Some("yourself"),
"am" => None, _ => None,
};
match flipped {
Some(f) => format!("{}{}{}", lead, f, tail),
None => token.to_string(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
pub fn canonical_description(body: &str) -> String {
let body = body.trim().trim_end_matches(['?', '.', '!']).trim();
let first_word = body.split_whitespace().next().unwrap_or("");
let mut chars = first_word.chars();
let sentence_case = matches!(chars.next(), Some(c) if c.is_uppercase())
&& chars.all(|c| c.is_lowercase())
&& first_word.chars().count() > 1;
if sentence_case {
let mut it = body.chars();
let first = it.next().expect("non-empty checked above");
format!("Remind me to {}{}", first.to_lowercase(), it.as_str())
} else {
format!("Remind me to {}", body)
}
}
pub fn confirmation_message(reminder: &Reminder, when: &str) -> String {
match reminder.connective {
Some(conn) => format!(
"⏰ Got it — I'll remind you {} {} {}.",
conn, reminder.body, when
),
None => format!("⏰ Got it — I'll remind you {}: {}.", when, reminder.body),
}
}
pub fn fire_message(reminder: &Reminder) -> String {
match reminder.connective {
Some("that") => format!("⏰ Reminder: remember that {}", reminder.body),
Some("about") | Some("of") => format!("⏰ Reminder about: {}", reminder.body),
_ => format!("⏰ Reminder: {}", reminder.body),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_basic_reminder_with_question_mark() {
let r = parse_reminder("Remind me to call my daughter?").unwrap();
assert_eq!(r.connective, Some("to"));
assert_eq!(r.body, "call your daughter");
}
#[test]
fn parses_polite_prefix() {
let r = parse_reminder("please remind me to take out the trash").unwrap();
assert_eq!(r.connective, Some("to"));
assert_eq!(r.body, "take out the trash");
}
#[test]
fn parses_about_connective() {
let r = parse_reminder("Remind me about the dentist appointment.").unwrap();
assert_eq!(r.connective, Some("about"));
assert_eq!(r.body, "the dentist appointment");
}
#[test]
fn parses_that_connective_with_pronoun_flip() {
let r = parse_reminder("remind me that I'm meeting Maria").unwrap();
assert_eq!(r.connective, Some("that"));
assert_eq!(r.body, "you're meeting Maria");
}
#[test]
fn parses_no_connective() {
let r = parse_reminder("remind me call mom").unwrap();
assert_eq!(r.connective, None);
assert_eq!(r.body, "call mom");
}
#[test]
fn rejects_non_reminders() {
assert!(parse_reminder("check the deploy status").is_none());
assert!(parse_reminder("send the weekly report").is_none());
assert!(parse_reminder("").is_none());
assert!(parse_reminder("list my reminders").is_none());
}
#[test]
fn rejects_recall_questions() {
assert!(parse_reminder("remind me what I said about the budget").is_none());
assert!(parse_reminder("remind me when my flight is").is_none());
assert!(parse_reminder("remind me how to restart the server").is_none());
}
#[test]
fn preserves_names_and_case_in_body() {
let r = parse_reminder("remind me to email Maria about Q3").unwrap();
assert_eq!(r.body, "email Maria about Q3");
}
#[test]
fn flips_possessives_and_contractions() {
assert_eq!(
flip_pronouns("pick up my dry cleaning"),
"pick up your dry cleaning"
);
assert_eq!(
flip_pronouns("tell me I'll be late"),
"tell you you'll be late"
);
assert_eq!(
flip_pronouns("that book is mine, not yours"),
"that book is yours, not yours"
);
}
#[test]
fn canonical_description_restores_reminder_form() {
assert_eq!(
canonical_description("Check logs"),
"Remind me to check logs"
);
assert_eq!(
canonical_description("check logs."),
"Remind me to check logs"
);
assert_eq!(canonical_description("PR review"), "Remind me to PR review");
let r = parse_reminder(&canonical_description("Call my daughter?")).unwrap();
assert_eq!(r.body, "call your daughter");
}
#[test]
fn confirmation_and_fire_messages_read_naturally() {
let r = parse_reminder("Remind me to call my daughter?").unwrap();
assert_eq!(
confirmation_message(&r, "today at 1:46 PM"),
"⏰ Got it — I'll remind you to call your daughter today at 1:46 PM."
);
assert_eq!(fire_message(&r), "⏰ Reminder: call your daughter");
let r2 = parse_reminder("remind me about the standup").unwrap();
assert_eq!(
confirmation_message(&r2, "tomorrow at 9:00 AM"),
"⏰ Got it — I'll remind you about the standup tomorrow at 9:00 AM."
);
assert_eq!(fire_message(&r2), "⏰ Reminder about: the standup");
}
#[test]
fn handles_wrapped_scheduled_task_description() {
assert!(parse_reminder(
"Execute scheduled goal: Remind me to call my daughter? [SYSTEM: already scheduled]"
)
.is_none());
}
}