## 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++ # 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.

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.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 != "") {
}
}
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)
}
}
```
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:
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 […]