My first significant Ruby program was this scheduler, sched.rb. It is based on similar ideas to my unfinished Perl scheduler, but this one does work. It executes $EDITOR to edit temporary files in order to enter recurrences, events, and tasks.
sched.rb
#!/usr/bin/ruby
require 'date'
require 'sqlite3'
class Filereader
def self.read_file(filename)
ret = Hash.new
key = ""
fin = File.open(filename, 'r')
begin
while line = fin.readline.chomp
if line =~ /^([^:]+): (.*)$/
key = $1
if not ret.key?(key)
ret[key] = [$2]
else
ret[key].push($2)
end
elsif key.length > 0 and line =~ /^\t(.*$)/
ret[key][-1] << "\n" << $1
end
end
rescue
fin.close
end
return ret
end
end
class Task
attr_reader :content, :status
def initialize(content, status)
@content = content
@status = status
end
end
class Event
attr_reader :name, :location, :date
def initialize(name, location, datestring)
@name = name
@location = location
@tasks = Array.new # Task objects
@time = 0
@datestring = datestring
if datestring.class.to_s == 'DateTime'
@date = datestring
elsif datestring =~ /^([0-9]{4})-([0-1][0-9])-([0-3][0-9])T([0-2][0-9]):([0-5][0-9]):[0-5][0-9]\+[0-2][0-4]:[0-5][0-9]$/
# Stripping out excess
@datestring = sprintf('%s-%s-%sT%s:%s', $1, $2, $3, $4, $5)
@date = DateTime.strptime(@datestring, '%Y-%m-%dT%H:%M')
elsif datestring =~ /^([0-9]{4})-([0-1][0-9])-([0-3][0-9])T([0-2][0-9]):([0-5][0-9])$/
@date = DateTime.strptime(datestring, '%Y-%m-%dT%H:%M')
elsif datestring =~ /^([0-9]{4})-([0-1][0-9])-([0-3][0-9])$/
@date = DateTime.strptime(datestring, '%Y-%m-%d')
elsif datestring =~ /^([0-9]{4})-([0-1][0-9])$/
@date = DateTime.strptime(datestring, '%Y-%m')
elsif datestring =~ /^([0-9]{4})$/
@date = DateTime.strptime(datestring, '%Y')
else
raise 'Event: invalid date: ' + datestring
end
end
def self.init_from_hash(hash)
if hash.key?('Name') and hash.key?('Location') and hash.key?('Date')
if hash['Name'][0].length > 0 and hash['Location'][0].length > 0 and hash['Date'][0].length > 0
begin
ret = Event.new(hash['Name'][0], hash['Location'][0], hash['Date'][0])
if hash.key?('Task')
hash['Task'].each do |task|
if task.length > 0
ret.add_task(Task.new(task, 0))
end
end
end
if hash.key?('Task[ ]')
hash['Task[ ]'].each do |task|
if task.length > 0
ret.add_task(Task.new(task, 0))
end
end
end
if hash.key?('Task[X]')
hash['Task[X]'].each do |task|
if task.length > 0
ret.add_task(Task.new(task, 1))
end
end
end
return ret
rescue
puts 'Event: Error: ' + $!
return nil
end
else
return nil
end
else
raise 'Event: Given hash is missing some elements.'
end
end
def date_string(fmt = '%Y-%m-%dT%H:%M')
return @date.strftime(fmt)
end
def datestring()
if @datestring.class.to_s == 'DateTime'
return @datestring.strftime('%Y-%m-%dT%H:%M')
else
return @datestring
end
end
def add_task(task)
@tasks.push(task)
end
def each_task
@tasks.each {|item| yield(item)}
end
end
class Recurrence
attr_reader :name, :location
def initialize(name, location)
@name = name
@location = location
@events = []
end
def add_event(event)
@events.push(event)
end
def each_event
@events.each {|item| yield(item)}
end
def self.generate_events(name, location, dates)
ret = Recurrence.new(name, location)
dates.each {|date| ret.add_event(Event.new(name, location, date))}
return ret
end
def self.init_from_hash(hash)
# Dates: weekly 0123456 time start end
# Dates: monthly [0-3][0-9] time start end
# Dates: yearly [0-1][0-9]-[0-3][0-9] time start end
if hash.key?('Name') and hash.key?('Location') and hash.key?('Dates')
if hash['Name'][0].length > 0 and hash['Location'][0].length > 0 and hash['Dates'][0].length > 0 and hash['Dates'].length > 0
begin
dparts = hash['Dates'][0].split(/\s/)
estart = Event.new(hash['Name'][0], hash['Location'][0], dparts[3])
eend = Event.new(hash['Name'][0], hash['Location'][0], dparts[4])
time = DateTime.strptime(dparts[2], '%H:%M')
dates = []
case dparts[0]
when 'weekly'
if dparts[1] =~ /^[0-6]+$/
tmpday = estart.date
while tmpday <= eend.date
if dparts[1].include?(tmpday.wday.to_s)
dates.push(DateTime.civil(tmpday.year, tmpday.month, tmpday.mday, time.hour, time.min))
end
tmpday = tmpday.next
end
else
raise 'Invalid weekday indices: ' + dparts[1]
end
when 'monthly'
mday = dparts[1].to_i
if mday >= 1 and mday <= 31
tmpday = estart.date
while tmpday <= eend.date
if tmpday.mday == mday
dates.push(DateTime.civil(tmpday.year, tmpday.month, tmpday.mday, time.hour, time.min))
end
tmpday = tmpday.next
end
else
raise 'Invalid month day index: ' + dparts[1]
end
when 'yearly'
if dparts[1] =~ /^([0-1][0-9])-([0-3][0-9])$/
month = $1.to_i
mday = $2.to_i
tmpday = estart.date
while tmpday <= eend.date
if tmpday.mday == mday and tmpday.month == month
dates.push(DateTime.civil(tmpday.year, tmpday.month, tmpday.mday, time.hour, time.min))
end
tmpday = tmpday.next
end
raise 'Invalid month/day index: ' + dparts[1]
end
else
raise 'Invalid recurrence operator: ' + dparts[0]
end
return Recurrence.generate_events(hash['Name'][0], hash['Location'][0], dates)
rescue
puts 'Recurrence: Error: ' + $!
return nil
end
else
return nil
end
else
raise 'Event: Given hash is missing some elements.'
end
end
end
def init_db(filename)
db = SQLite3::Database.new(filename)
db.execute('PRAGMA auto_vacuum = 1')
db.execute('CREATE TABLE IF NOT EXISTS recurrences(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, location TEXT)')
db.execute('CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, location TEXT, date TEXT, recurrence INTEGER)')
db.execute('CREATE TABLE IF NOT EXISTS tasks(id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, status INTEGER, event INTEGER)')
return db
end
def list_events(db, selectors, glob)
query = 'SELECT * FROM events'
args = []
first = 1
if selectors.length > 0
query << ' WHERE'
end
selectors.each do |key, value|
query << (first > 0 ? ' ' : ' AND ') + key + (glob == 1 ? ' GLOB ?' : ' LIKE ?')
args.push(value)
first -= 1
end
db.execute(query, *args) do |row|
ev = Event.new(row[1], row[2], row[3])
# printf "%6d %30s %20s %15s\n", row[0], row[1], row[2], row[3]
printf "%6d %30s %20s %25s\n", row[0], row[1], row[2], ev.date_string('%H:%M on %a., %b. %d, %Y')
i = 1
db.execute('SELECT tasks.content, tasks.status FROM tasks WHERE event = ?', row[0]) do |task|
printf "\t%6d %60s %5s\n", i, task[0], (task[1].to_i == 1 ? 'Done' : 'Prog.')
i += 1
end
end
end
def list_tasks(db, selectors, glob)
query = 'SELECT tasks.id, tasks.content, tasks.status, events.date, events.name FROM tasks LEFT OUTER JOIN events ON tasks.event = events.id'
args = []
first = 1
if selectors.length > 0
query << ' WHERE'
end
selectors.each do |key, value|
query << (first > 0 ? ' ' : ' AND ') + key + (glob == 1 ? ' GLOB ?' : ' LIKE ?')
args.push(value)
first -= 1
end
db.execute(query, *args) do |row|
ev = Event.new('', '', row[3])
printf "%6d %60s %20s %5s %25s\n", row[0], row[1], row[4], (row[2].to_i == 1 ? 'Done' : 'Prog.'), ev.date_string('%H:%M on %a., %b. %d, %Y')
end
end
def parse_selectors(args)
selectors = Hash.new
svars = Hash.new
args.each do |arg|
if arg[0..1] == '--'
parts = arg[2..-1].split(/=/)
if parts.length == 1 and arg[2..-1] =~ /=/
parts << ''
end
yield(selectors, svars, parts)
else
raise 'Unknown option: ' + arg
end
end
return selectors, svars
end
if ARGV.length > 0 and ARGV[0][0..1] == '--' and ENV.key?('EDITOR')
db = nil
tmpfile = nil
begin
case ARGV[0][2..-1]
when 'create-event'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
# Create tmpfile
tmpfile = %x[mktemp].chomp
f = File.open(tmpfile, 'w+')
f.print("Name: \nLocation: \nDate: \n")
f.close
# Edit tmpfile
m1 = File.mtime(tmpfile);
system(ENV['EDITOR'], tmpfile)
m2 = File.mtime(tmpfile);
if m2 != m1
# Read tmpfile
data = Filereader.read_file(tmpfile)
event = Event.init_from_hash(data)
if event
db.execute('INSERT INTO events(name, location, date) VALUES(?, ?, ?)', event.name, event.location, event.datestring)
db.execute('SELECT events.id FROM events WHERE name = ? AND location = ? AND date = ?', event.name, event.location, event.datestring) do |row|
event.each_task do |task|
db.execute('INSERT INTO tasks(content, status, event) VALUES(?, ?, ?)', task.content, task.status, row[0])
end
end
else
$stderr.puts "Null event given; skipping."
end
else
$stderr.puts "Null event given; skipping."
end
File.delete(tmpfile)
when 'create-recurrence'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
# Create tmpfile
tmpfile = %x[mktemp].chomp
f = File.open(tmpfile, 'w+')
f.print("Name: \nLocation: \nDates: \n")
f.close
# Edit tmpfile
m1 = File.mtime(tmpfile);
system(ENV['EDITOR'], tmpfile)
m2 = File.mtime(tmpfile);
if m2 != m1
# Read tmpfile
data = Filereader.read_file(tmpfile)
File.delete(tmpfile)
recurrence = Recurrence.init_from_hash(data)
if recurrence
db.execute('INSERT INTO recurrences(name, location) VALUES(?, ?)', recurrence.name, recurrence.location)
db.execute('SELECT recurrences.id FROM recurrences WHERE name = ? AND location = ?', recurrence.name, recurrence.location) do |rid|
recurrence.each_event do |event|
db.execute('INSERT INTO events(name, location, date, recurrence) VALUES(?, ?, ?, ?)', event.name, event.location, event.datestring, rid)
db.execute('SELECT events.id FROM events WHERE name = ? AND location = ? AND date = ? AND recurrence = ?', event.name, event.location, event.datestring, rid) do |row|
event.each_task do |task|
db.execute('INSERT INTO tasks(content, status, event) VALUES(?, ?, ?)', task.content, task.status, row[0])
end
end
end
end
else
$stderr.puts "Null recurrence given; skipping."
end
else
$stderr.puts "Null recurrence given; skipping."
end
when 'edit-event'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
selectors = Hash.new
selectors, svars = parse_selectors(ARGV[1..-1]) do |selectors, svars, parts|
option = parts[0]
value = parts[1..-1].join('=')
case option
when 'name', 'location', 'date', 'id'
selectors['events.' + option] = (value ? value : '')
else
raise 'Unknown selector: ' + option
end
end
query = 'SELECT * FROM events'
args = []
first = 1
if selectors.length > 0
query << ' WHERE'
end
selectors.each do |key, value|
query << (first > 0 ? ' ' : ' AND ') + key + ' LIKE ?'
args.push(value)
first -= 1
end
db.execute(query, *args) do |row|
# Create tmpfile
id = row[0]
tmpfile = %x[mktemp].chomp
f = File.open(tmpfile, 'w+')
first = 1
f.print 'Name: '
row[1].split(/\n/).each do |line|
if first
f.puts line
first = 0
else
f.puts "\t" + line
end
end
first = 1
f.print 'Location: '
row[2].split(/\n/).each do |line|
if first
f.puts line
first = 0
else
f.puts "\t" + line
end
end
first = 1
f.print 'Date: '
row[3].split(/\n/).each do |line|
if first
f.puts line
first = 0
else
f.puts "\t" + line
end
end
db.execute('SELECT * FROM tasks WHERE tasks.event = ?', id) do |task|
first = 1
f.print 'Task[' + (task[2].to_i == 1 ? 'X' : ' ') + ']: '
task[1].split(/\n/).each do |line|
if first
f.puts line
first = 0
else
f.puts "\t" + line
end
end
end
f.close
# Edit tmpfile
m1 = File.mtime(tmpfile);
system(ENV['EDITOR'], tmpfile)
m2 = File.mtime(tmpfile);
if m2 != m1
# Read tmpfile
data = Filereader.read_file(tmpfile)
event = Event.init_from_hash(data)
if event
db.execute('UPDATE events SET name = ?, location = ?, date = ? WHERE id = ?', event.name, event.location, event.datestring, id)
db.execute('DELETE FROM tasks WHERE tasks.event = ?', id)
event.each_task do |task|
db.execute('INSERT INTO tasks(content, status, event) VALUES(?, ?, ?)', task.content, task.status, row[0])
end
else
$stderr.puts "Null event given; skipping."
end
else
$stderr.puts "Null event given; skipping."
end
File.delete(tmpfile)
end
when 'delete-event'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
selectors, svars = parse_selectors(ARGV[1..-1]) do |selectors, svars, parts|
option = parts[0]
value = parts[1..-1].join('=')
case option
when 'name', 'location', 'date', 'id'
selectors['events.' + option] = (value ? value : '')
else
raise 'Unknown selector: ' + option
end
end
query = 'SELECT events.id FROM events'
args = []
first = 1
if selectors.length > 0
query << ' WHERE'
end
selectors.each do |key, value|
query << (first > 0 ? ' ' : ' AND ') + key + ' LIKE ?'
args.push(value)
first -= 1
end
db.execute(query, *args) do |row|
db.execute('DELETE FROM events WHERE events.id = ?', row[0])
db.execute('DELETE FROM tasks WHERE tasks.event = ?', row[0])
end
when 'delete-recurrence'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
selectors = Hash.new
recursive = 0
selectors, svars = parse_selectors(ARGV[1..-1]) do |selectors, svars, parts|
if parts.length >= 2
option = parts[0]
value = parts[1..-1].join('=')
case option
when 'name', 'location', 'date', 'id'
selectors['events.' + option] = (value ? value : '')
else
raise 'Unknown selector: ' + option
end
elsif parts.length == 1
option = parts[0]
case option
when 'recursive'
svars['recursive'] = 1
else
raise 'Unknown option: --' + parts.join('=')
end
end
end
query = 'SELECT recurrences.id FROM recurrences'
args = []
first = 1
if selectors.length > 0
query << ' WHERE'
end
selectors.each do |key, value|
query << (first > 0 ? ' ' : ' AND ') + key + ' LIKE ?'
args.push(value)
first -= 1
end
db.execute(query, *args) do |row|
if svars['recursive'] == 1 # Recursive deletion
db.execute('SELECT events.id FROM events WHERE events.recurrence = ?', row[0]) do |event|
db.execute('DELETE FROM events WHERE id = ?', event[0])
db.execute('DELETE FROM tasks WHERE event = ?', event[0])
end
else # Disconnects events from the recurrence
db.execute('UPDATE events SET recurrence = ? WHERE recurrence = ?', -1, row[0])
end
end
when 'list-events'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
selectors, svars = parse_selectors(ARGV[1..-1]) do |selectors, svars, parts|
if parts.length >= 2
option = parts[0]
value = parts[1..-1].join('=')
case option
when 'name', 'location', 'date'
selectors['events.' + option] = (value ? value : '')
else
raise 'Unknown selector: ' + option
end
elsif parts.length == 1
option = parts[0]
case option
when 'glob'
svars['glob'] = 1
else
raise 'Unknown option: --' + parts.join('=')
end
end
end
list_events(db, selectors, svars['glob'])
when 'list-tasks'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
selectors = Hash.new
glob = 0
selectors, svars = parse_selectors(ARGV[1..-1]) do |selectors, svars, parts|
if parts.length >= 2
option = parts[0]
value = parts[1..-1].join('=')
case option
when 'name', 'location', 'date'
selectors['events.' + option] = (value ? value : '')
when 'content', 'status'
selectors['tasks.' + option] = (value ? value : '')
else
raise 'Unknown selector: ' + option
end
elsif parts.length == 1
option = parts[0]
case option
when 'glob'
svars['glob'] = 1
else
raise 'Unknown option: --' + parts.join('=')
end
end
end
list_tasks(db, selectors, svars['glob'])
when 'complete-tasks'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
selectors = Hash.new
glob = 0
selectors, svars = parse_selectors(ARGV[1..-1]) do |selectors, svars, parts|
if parts.length >= 2
option = parts[0]
value = parts[1..-1].join('=')
case option
when 'content', 'id', 'event'
selectors[option] = (value ? value : '')
else
raise 'Unknown selector: ' + option
end
elsif parts.length == 1
option = parts[0]
case option
when 'glob'
svars['glob'] = 1
else
raise 'Unknown option: --' + parts.join('=')
end
end
end
query = 'UPDATE tasks SET status = 1'
args = []
first = 1
if selectors.length > 0
query << ' WHERE'
end
selectors.each do |key, value|
query << (first > 0 ? ' ' : ' AND ') + key + (glob == 1 ? ' GLOB ?' : ' LIKE ?')
args.push(value)
first -= 1
end
db.execute(query, *args)
when 'list-recurrences'
db = init_db(File.join(ENV['HOME'], '.sched_db'))
else
$stderr.puts 'Unknown command: ' + ARGV[0]
end
rescue
$stderr.puts 'Error: ' + $!
ensure
if db
db.close
end
end
else
$stderr.puts 'Usage: ' + $0 + ' [ --create-event | --create-recurrence | --edit-event | --delete-event | --delete-recurrence | --list-events | --list-tasks | --list-recurrences ] ...'
$stderr.puts "\tEDITOR must be set: currently \"" + (ENV['EDITOR'] and ENV['EDITOR'] or '') + "\""
end