Last active
August 29, 2015 14:08
-
-
Save samullen/213df109bbffb2240d2e to your computer and use it in GitHub Desktop.
Very basic todo script for use in an "Intro to Ruby" project I'm developing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require "date" | |
class Todo | |
attr_accessor :complete | |
attr_reader :name, :due_on | |
def initialize(name, due_on=nil, complete=false) | |
@name = name | |
@due_on = due_on | |
@complete = complete | |
end | |
def self.from_string(string) | |
todo = string.split("::") | |
complete = todo[0] == "x" | |
name = todo[1] | |
due_on = todo[2] ? Date.parse(todo[2]) : nil | |
self.new(name, due_on, complete) | |
end | |
def complete! | |
self.complete = true | |
end | |
def complete? | |
self.complete == true | |
end | |
def to_s | |
complete = self.complete? ? "x" : "-" | |
[complete, self.name, self.due_on].join("::") | |
end | |
end | |
class TodoList | |
attr_reader :todos | |
TODOPATH = File.join(ENV["HOME"], "todos.txt") | |
def initialize | |
@todos = [] | |
load_todos | |
end | |
def all | |
self.todos | |
end | |
def add(name, due_on) | |
self.todos << Todo.new(name, due_on) | |
save_todos | |
end | |
def complete(index) | |
self.todos[index].complete! | |
save_todos | |
end | |
def move(start_index, end_index) | |
todo = self.todos.delete_at(start_index) | |
self.todos.insert(end_index, todo) | |
save_todos | |
end | |
def delete(index) | |
self.todos.delete_at(index) | |
save_todos | |
end | |
private | |
def load_todos | |
File.open(TODOPATH, "a+") do |file| | |
file.each do |line| | |
self.todos << Todo.from_string(line.chomp) | |
end | |
end | |
end | |
def save_todos | |
File.open(TODOPATH, "w") do |file| | |
self.todos.each do |todo| | |
file.puts todo.to_s | |
end | |
end | |
end | |
end | |
todo_list = TodoList.new | |
def list_todos(todo_list) | |
todo_list.all.each_with_index do |todo, index| | |
complete = todo.complete? ? "x" : " " | |
line = "#{index + 1}. [#{complete}] #{todo.name}" | |
line += " - #{todo.due_on}" if todo.due_on | |
puts line | |
end | |
end | |
def puts_title(title) | |
puts title | |
puts "-" * title.length | |
end | |
puts | |
case ARGV[0] | |
when "list" | |
puts_title "Listing todos" | |
list_todos(todo_list) | |
when "add" | |
puts_title "Adding a todo" | |
todo_list.add(ARGV[1], ARGV[2]) | |
list_todos(todo_list) | |
when "complete" | |
puts_title "Completing todo #{ARGV[1]}" | |
todo_list.complete(ARGV[1].to_i - 1) | |
list_todos(todo_list) | |
when "move" | |
puts_title "Moving todo #{ARGV[1]} to #{ARGV[2]}" | |
todo_list.move(ARGV[1].to_i - 1, ARGV[2].to_i - 1) | |
list_todos(todo_list) | |
when "delete" | |
puts_title "Deleting todo #{ARGV[1]}" | |
todo_list.delete(ARGV[1].to_i - 1) | |
list_todos(todo_list) | |
else | |
puts <<-EOL | |
Usage: todo.rb <option> [option argument(s)] | |
list # List the todo items in your list | |
add <name> [due date] # Add new todo to your list with optional due date | |
complete <position> # Mark a todo complete | |
move <pos> <new pos> # Move a todo to a different position | |
delete <position> # Remove a todo at a position | |
help | |
EOL | |
end | |
puts |
Quick annotation, only paying attention to details of the code rather than the whole of it. I can go back over it later with that in mind:
#!/usr/bin/env ruby
# Is that even necessary to require?
require "date"
class Todo
attr_accessor :complete
attr_reader :name, :due_on
# Avoid nil like the plague. Even if you did use it, these will
# still be nil if you leave off the default there
def initialize(name, due_on=nil, complete=nil)
@name = name
# Short circuiting can come in handy here:
# @due_on = due_on && Date.parse(due_on)
@due_on = due_on ? Date.parse(due_on.to_s) : nil
# Just do a boolean check on it, no reason to || false.
@complete = complete || false
end
def self.from_string(string)
# I'd agree with avoiding regex unless absolutely necessary.
# It tends to get hairy fast.
string.match(/\A(x|-) (.+?)\s*(\d{4}-\d\d-\d\d)?\z/)
# Look into the english regex terms, assume perlisms like $1 are off-limits
complete = $1 == "x"
name = $2
due_on = $3
self.new(name, due_on, complete)
end
def complete!
self.complete = true
end
# You can just use self.complete here
def complete?
self.complete == true
end
end
class TodoList
attr_reader :todos
def initialize
@todos = []
load_todos
end
# Look into Enumerable, it'll allow you to iterate over things by
# defining an each method, and a comparator (<=>)
def all
self.todos
end
# Look into after-method hooks so you can abstract the save_todos
# method into being executed after certain methods.
def add(name, due_on)
self.todos << Todo.new(name, due_on)
save_todos
end
def complete(index)
self.todos[index.to_i - 1].complete!
save_todos
end
# Whole words won't kill anyone. Type them out instead of using odd
# abbreviations.
def move(start_idx, end_idx)
todo = self.todos.delete_at(start_idx.to_i - 1)
self.todos.insert(end_idx.to_i - 1, todo)
save_todos
end
def delete(index)
self.todos.delete_at(index.to_i - 1)
save_todos
end
private
def load_todos
# Make the file path a constant in the head of this
File.open(File.join(ENV["HOME"], "todos.txt"), "a+") do |file|
# Consider the use of map instead:
#
# self.todos.concat file.map { |line| Todo.from_string(line.chomp) }
file.each do |line|
self.todos << Todo.from_string(line.chomp)
end
end
end
def save_todos
File.open(File.join(ENV["HOME"], "todos.txt"), "w") do |file|
self.todos.each do |todo|
file.puts "%c %s %s" % [todo.complete? ? "x" : "-", todo.name, todo.due_on]
end
end
end
end
todo_list = TodoList.new
def list_todos(todo_list)
# If you do that Enumerable bit above, you can get rid of all
todo_list.all.each_with_index do |todo, index|
# This ternary is repeated multiple times. Abstract it to a method instead
line = "%.2d [%c] %s" % [index + 1, todo.complete? ? "x" : " ", todo.name]
# use <<, as it's faster
line += " - %s" % todo.due_on.strftime("%b %d, %Y") if todo.due_on
puts line
end
end
puts
# Might want to chomp that, newlines make for a bad time.
case ARGV[0]
when "list"
# nifty method idea for formatting:
#
# def puts_title(string)
# puts string
# puts '-' * string.length
# end
puts "Listing todos"
puts "-------------"
list_todos(todo_list)
when "add"
puts "Adding a todo"
puts "-------------"
todo_list.add(ARGV[1], ARGV[2])
list_todos(todo_list)
when "complete"
puts "Completing todo #{ARGV[1]}"
puts "------------------"
todo_list.complete(ARGV[1])
list_todos(todo_list)
when "move"
puts "Moving todo #{ARGV[1]} to #{ARGV[2]}"
puts "--------------------"
todo_list.move(ARGV[1], ARGV[2])
list_todos(todo_list)
when "delete"
puts "Deleting todo #{ARGV[1]}"
puts "----------------"
todo_list.delete(ARGV[1])
list_todos(todo_list)
else
puts <<-EOL
Usage: todo.rb <option> [option argument(s)]
list # List the todo items in your list
add <name> [due date] # Add new todo to your list with optional due date
complete <position> # Mark a todo complete
move <pos> <new pos> # Move a todo to a different position
delete <position> # Remove a todo at a position
help
EOL
end
puts
Great! Thanks everyone for your input. I implemented most of the suggestions, but not all; some was left in for the purpose of illustration, even if not the "Ruby Way"(tm).
I really appreciate the help and constructive feedback.
Looks great. I am a bit surprised that people feel regex is seldom used. It is a good skill to have, but definitely a subject in and of itself :P
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oh... and I disagree with
ToDo
, it's just awkward. There are plenty of usages of "todo" without a hyphen or a space, so just keep it one word.