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