Remind
July 2, 2019
NAME
remind -- print reminders of upcoming events
USAGE
remind -- show reminders for next seven days
remind [year] month day message -- add reminder to database
DESCRIPTION
Remind maintains a database of reminders in the .reminders file,
in the user's home directory, each a single line of the form
[year] month day message
Year is optional, and must be an integer greater than 99; if no
year is given, the reminder applies to all years (for instance,
birthdays).
If remind is called with no arguments, it writes to standard
output all reminders that occur within the next seven days. If
remind is called with arguments giving a date and message, a
reminder is added to the database. Any time remind is called,
all past reminders are deleted from the database.
EXAMPLE
$ date
Sun Jun 30 19:45:38 CDT 2019
$ remind 4 2 Anne birthday
$ remind 10 13 Kate birthday
$ remind 7 4 Independence Day
$ remind 2019 7 2 lunch with Pat
$ remind 2019 5 13 dentist 2:00pm
$ remind
7 4 Independence Day
2019 7 2 lunch with Pat
$ cat ./reminders
4 2 Anne birthday
10 13 Kate birthday
7 4 Independence Day
2019 7 2 lunch with Pat
Your task is to implement remind. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.
Rust version – good exercise! I learned a lot about Rust dates, sorting, file I/O, error handling…
use itertools::Itertools; use chrono::prelude::*; fn main() -> Result<(), String> { let mut r = Reminders::new(".reminders")?; let args = std::env::args().skip(1); if args.len() == 0 { print!("{}", r.stringify(7)); } else { r.add(r.parse_item(args)?); } r.close() } #[derive(Debug)] struct Reminders { path: std::path::PathBuf, today: NaiveDate, reminder_items: Vec<ReminderItem>, } #[derive(Debug)] struct ReminderItem { date: NaiveDate, recurring: bool, message: String, } impl Reminders { fn new(path_str: &str) -> Result<Self, String> { let mut path = match dirs::home_dir() { Some(dir) => dir, None => return Err("could not find home directory!".to_string()) }; path.push(path_str); let mut reminder = Reminders { path, today: Local::today().naive_local(), reminder_items: vec!() }; if let Ok(data) = std::fs::read_to_string(&reminder.path) { for line in data.split("\n").filter(|&l| l != "") { reminder.add(reminder.parse_item(line.split(" ").collect::<Vec<_>>().into_iter())?); } } Ok(reminder) } fn add(&mut self, item: ReminderItem) { if item.date.num_days_from_ce() >= self.today.num_days_from_ce() { self.reminder_items.push(item); self.reminder_items.sort_unstable_by_key(|item| item.date); } } fn stringify(&self, ndays: i32) -> String { let max_day = self.today.num_days_from_ce() + ndays; self.reminder_items .iter() .filter(|item| ndays == 0 || item.date.num_days_from_ce() < max_day) .map(|i| i.to_string() + "\n") .join("") } fn close(self) -> Result<(), String> { match std::fs::write(&self.path, self.stringify(0)) { Err(m) => Err(format!("could not write reminders to {}: {}", self.path.display(), m)), _ => Ok(()) } } fn parse_item<I, T>(&self, mut args: I) -> Result<ReminderItem, String> where I: Iterator<Item=T> + ExactSizeIterator, T: std::fmt::Display, { let usage = Err("usage: remind [year] month day message".to_string()); let mut arg = args.next(); let year = match &arg { Some(year) => { match year.to_string().parse::<i32>() { Ok(year) if year > 99 => { arg = args.next(); Some(year) } Ok(_) => None, _ => return usage } } None => return usage }; if args.len() < 2 { return usage; } let month = match arg.unwrap().to_string().parse::<u32>() { Ok(month) => month, _ => return usage }; let day = match args.next().unwrap().to_string().parse::<u32>() { Ok(day) => day, _ => return usage }; let date = if let Some(year) = year { NaiveDate::from_ymd_opt(year, month, day) } else { self.next_recurring_date(month, day) }; if let Some(date) = date { Ok(ReminderItem{ date, recurring: year.is_none(), message: args.join(" ") }) } else { usage } } fn next_recurring_date(&self, month: u32, day: u32) -> Option<NaiveDate> { let mut year = self.today.year(); if month == 2 && day == 29 { loop { if let Some(date) = NaiveDate::from_ymd_opt(year, 2, 29) { if date.num_days_from_ce() >= self.today.num_days_from_ce() { break Some(date); } } year += 1; } } else if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) { if date.num_days_from_ce() >= self.today.num_days_from_ce() { Some(date) } else { NaiveDate::from_ymd_opt(year + 1, month, day) } } else { None } } } impl std::fmt::Display for ReminderItem { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { if !self.recurring { write!(f, "{} ", self.date.year())?; } write!(f, "{} {} {}", self.date.month(), self.date.day(), self.message) } }Github: https://github.com/wpwoodjr/remind
Here’s a solution in Python.
from datetime import datetime, timedelta import os from pathlib import Path import re import sys DATABASE_PATH = os.path.join(os.path.expanduser('~'), '.reminders') PATTERN = re.compile('^(?:(\d{3,4}) )?(\d{1,2}) (\d{1,2}) (.*)(?:\n)?$') NOW = datetime.now() class Reminder: def __init__(self, year, month, day, message): self.year = year self.month = month self.day = day self.message = message @staticmethod def from_line(line): m = re.match(PATTERN, line) if m is None: return None year, month, day, message = m.groups() year = int(year) if year else year month = int(month) day = int(day) reminder = Reminder(year, month, day, message) return reminder def expired(self): if self.year is None: return False reminder_ymd = datetime(self.year, self.month, self.day) now_ymd = datetime(NOW.year, NOW.month, NOW.day) return reminder_ymd < now_ymd def active(self): now_ymd = datetime(NOW.year, NOW.month, NOW.day) if self.year is None: reminder_ymd = datetime(NOW.year, self.month, self.day) if reminder_ymd < now_ymd: reminder_ymd = datetime(NOW.year + 1, self.month, self.day) else: reminder_ymd = datetime(self.year, self.month, self.day) next_week_ymd = now_ymd + timedelta(days=7) return now_ymd <= reminder_ymd <= next_week_ymd def __str__(self): s = f'{self.year} ' if self.year is not None else '' s += f'{self.month} {self.day} {self.message}' return s class Reminders: def __init__(self, path): self.path = path self.reminders = [] try: with open(DATABASE_PATH) as f: lines = f.readlines() for line in lines: reminder = Reminder.from_line(line) self.reminders.append(reminder) except FileNotFoundError: Path(DATABASE_PATH).touch() def append(self, reminder): self.reminders.append(reminder) with open(DATABASE_PATH, 'a') as f: f.write(f'{reminder}\n') def drop_expired(self): self.reminders = [reminder for reminder in self.reminders if not reminder.expired()] with open(DATABASE_PATH, 'w') as f: lines = [f'{reminder}\n' for reminder in self.reminders] f.writelines(lines) def print_active(self): for reminder in self.reminders: if not reminder.active(): continue print(reminder) reminders = Reminders(DATABASE_PATH) reminders.drop_expired() if len(sys.argv) == 1: reminders.print_active() else: reminder = Reminder.from_line(' '.join(sys.argv[1:])) if reminder is None: sys.exit(os.EX_USAGE) reminders.append(reminder) sys.exit(os.EX_OK)[…] an excellent introduction to Unix, still relevant today even though it was published in 1984. The recent exercise Remind was inspired by a program in Section 4.4, and today’s exercise is a rewrite of the program in […]