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.

Pages: 1 2

3 Responses to “Remind”

  1. Bill Wood said

    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

  2. Daniel said

    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)
    
  3. […] 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 […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: