Skip to content

Instantly share code, notes, and snippets.

@samullen
Last active August 29, 2015 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samullen/213df109bbffb2240d2e to your computer and use it in GitHub Desktop.
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.
#!/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
@coderkevin
Copy link

Hi Samuel,

I also agree with @bellmyer. There's a lot here--good stuff, but I'd want at least 1 hour plus time for questions to teach an OO-savvy programmer crowd (coming from Java, C#, C++, Python, etc.) If this is for a crowd that hasn't done OO it would take more, and for a non-programmer crowd this should be broken up into several lessons.

Are you planning on evolving the code by DRYing it as part of the talk? That might be a good idea. Or maybe a test or two...but then again, only 30 minutes is a very short time.

@samullen
Copy link
Author

samullen commented Nov 1, 2014

@bellmyer, @coderkevin: it's hard to explain and I'm still working on things. It might be that I spread out creating the code above across different things. There is a lot to what has been written, and yes everything in total is going to take between three and five hours, but could writing the above code from start to finish while explaining what I was doing take less then thirty minutes.

@samullen
Copy link
Author

samullen commented Nov 1, 2014

@bellmyer and as a followup, the assumption is that those participating will have some knowledge of programming. Maybe not OOP, but at least an understanding of variables, control structures, etc.

@samullen
Copy link
Author

samullen commented Nov 1, 2014

Thanks, @keithrbennet. Lots of good stuff there, and I really appreciate you taking the time necessary to get such great feedback.

@darrencauthon
Copy link

@samullen The only advice I'll offer is to drop the the regex. Just parse the string with a split on some whitespace character or something simple. Move list_todos into the object itself, perhaps ahh.....

class Todo
  def self.from_string(string)
    complete, name, due_on = string.split("\t")
    complete = complete == "x"
    self.new(name, due_on, complete)
  end

  def to_s
    [(complete ? "x" : "-"), name, due_on].join("\t")
  end
end

Regex sucks, its hard, and its very rare. Don't scare Ruby newbiews with it. Yeah, Ruby has regex, but every other language has it, too.

@darrencauthon
Copy link

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.

@baweaver
Copy link

baweaver commented Nov 2, 2014

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 

@samullen
Copy link
Author

samullen commented Nov 3, 2014

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.

@4ydx
Copy link

4ydx commented Nov 8, 2014

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