Remind

July 2, 2019

#! /bin/sh

REMINDERFILE=/home/$(whoami)/.reminders
TEMPFILE=$(mktemp)

if   [ $# -gt 0 ]; then PRINTING=0; echo "$*" >> $REMINDERFILE
elif [ ! -f $REMINDERFILE ]; then PRINTING=1; touch $REMINDERFILE
else PRINTING=1; fi

gawk '

    # _The_Awk_Programming_Language_ by Aho, Kernighan and Weinberger
    # daynum function from solution to Exercise 3.8
    # only valid from 1901 to 2099; performs no validation
    function daynum(y, m, d,    days, i, n) { # 1 == Jan 1, 1901
        split("31 28 31 30 31 30 31 31 30 31 30 31", days)
        # 365 days per year, plus one for each leap year
        n = (y-1901) * 365 + int((y-1901)/4)
        if (y % 4 == 0) days[2]++ # leap year from 1901 to 2099
        for (i = 1; i = today) {
        print $0 >> "'$TEMPFILE'" }

    printing && $1 < 100 &&
    today <= daynum(year, $1, $2) &&
    daynum(year, $1, $2) = 100 &&
    today <= daynum($1, $2, $3) &&
    daynum($1, $2, $3) <= today + 7

' $REMINDERFILE

mv $TEMPFILE $REMINDERFILE

# PLB 6/30/2019

That’s not bad — a genuinely useful program in half a page of code. You can run the program at https://ideone.com/xuRsYh.

Advertisements

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: